diff --git a/.github/workflows/branch.yml b/.github/workflows/branch.yml index ebf6a56f..23a3facc 100644 --- a/.github/workflows/branch.yml +++ b/.github/workflows/branch.yml @@ -40,6 +40,15 @@ jobs: jar-artifact-name: tp2intervals-jar image-name: tp2intervals image-tag: ${{ needs.extract_branch_name.outputs.branch_name }} + + docker-image-new: + needs: + - jar + uses: ./.github/workflows/workflow-docker-image-new.yml + secrets: inherit + with: + jar-name: tp2intervals + jar-artifact-name: tp2intervals-jar dry-run: false electron: diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index d781753b..2d57c09d 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -19,11 +19,10 @@ jobs: docker-image: needs: - jar - uses: ./.github/workflows/workflow-docker-image.yml + uses: ./.github/workflows/workflow-docker-image-new.yml with: jar-name: tp2intervals jar-artifact-name: tp2intervals-jar - image-name: tp2intervals electron: needs: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2ff96bd9..497d58a3 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -26,6 +26,16 @@ jobs: image-tag: latest dry-run: false + docker-image-new: + needs: + - jar + uses: ./.github/workflows/workflow-docker-image-new.yml + secrets: inherit + with: + jar-name: tp2intervals + jar-artifact-name: tp2intervals-jar + dry-run: false + electron: needs: - jar diff --git a/.github/workflows/workflow-docker-image-new.yml b/.github/workflows/workflow-docker-image-new.yml new file mode 100644 index 00000000..31703f5d --- /dev/null +++ b/.github/workflows/workflow-docker-image-new.yml @@ -0,0 +1,115 @@ +name: Docker Image + +# This workflow uses actions that are not certified by GitHub. +# They are provided by a third-party and are governed by +# separate terms of service, privacy policy, and support +# documentation. + +on: + workflow_call: + inputs: + jar-name: + required: true + type: string + jar-artifact-name: + required: true + type: string + dry-run: + required: false + type: boolean + default: true + +env: + # Use docker.io for Docker Hub if empty + REGISTRY: ghcr.io + # github.repository as / + IMAGE_NAME: ${{ github.repository }} + + +jobs: + build: + + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + # This is used to complete the identity challenge + # with sigstore/fulcio when running outside of PRs. + id-token: write + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - uses: actions/download-artifact@master + with: + name: ${{ inputs.jar-artifact-name }} + path: artifact + + # Install the cosign tool except on PR + # https://github.com/sigstore/cosign-installer + - name: Install cosign + if: ${{ inputs.dry-run == false }} + uses: sigstore/cosign-installer@v3.8.1 + with: + cosign-release: 'v2.2.4' + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + # Set up BuildKit Docker container builder to be able to build + # multi-platform images and export cache + # https://github.com/docker/setup-buildx-action + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 # v3.0.0 + + # Login against a Docker registry except on PR + # https://github.com/docker/login-action + - name: Log into registry ${{ env.REGISTRY }} + if: ${{ inputs.dry-run == false }} + uses: docker/login-action@v3 # v3.0.0 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + # Extract metadata (tags, labels) for Docker + # https://github.com/docker/metadata-action + - name: Extract Docker metadata + id: meta + uses: docker/metadata-action@v5 # v5.0.0 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + type=raw,value=latest,enable={{is_default_branch}} + type=ref,event=tag + + # Build and push Docker image with Buildx (don't push on PR) + # https://github.com/docker/build-push-action + - name: Build and push Docker image + id: build-and-push + uses: docker/build-push-action@v6 # v5.0.0 + with: + context: . + push: ${{ inputs.dry-run == false }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max + platforms: linux/amd64,linux/arm64 + build-args: "JAR_PATH=artifact/${{ inputs.jar-name }}.jar" + + # Sign the resulting Docker image digest except on PRs. + # This will only write to the public Rekor transparency log when the Docker + # repository is public to avoid leaking data. If you would like to publish + # transparency data even for private images, pass --force to cosign below. + # https://github.com/sigstore/cosign + - name: Sign the published Docker image + if: ${{ inputs.dry-run == false }} + env: + # https://docs.github.com/en/actions/security-guides/security-hardening-for-github-actions#using-an-intermediate-environment-variable + TAGS: ${{ steps.meta.outputs.tags }} + DIGEST: ${{ steps.build-and-push.outputs.digest }} + # This step uses the identity token to provision an ephemeral certificate + # against the sigstore community Fulcio instance. + run: echo "${TAGS}" | xargs -I {} cosign sign --yes {}@${DIGEST} diff --git a/CHANGELOG.md b/CHANGELOG.md index 0d30c3d2..455bb6c3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,22 @@ - +### 0.12.2 +- Fixed TP Premium users coulndt select future dates to sync workouts + +### 0.12.1 +- Fixed invisible remove button for scheduled job + +### 0.12.0 +- Multiarch docker image +- Persistent scheduled job + +### 0.11.1 +- Fix docker image + +### 0.11.0 +- Added scheduled job to sync planned workouts in calendars +- Added support arm64 for Mac +- Added support to skip already synced workouts when syncing to Intervals.icu +- Fixed null workout name in TrainingPeaks + ### 0.10.0 - Added calendar sync between TrainerRoad, TrainingPeaks and Intervals - Added bash script for automatic calendar sync with cron @@ -9,6 +27,5 @@ - Added TrainerRoad configuration tutorial - Removed Strava support - ### 0.9.0 -- Added support distance based steps in workouts \ No newline at end of file +- Added support distance based steps in workouts diff --git a/Dockerfile b/Dockerfile index 1d488804..0877f447 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,5 +3,4 @@ EXPOSE 8080 ARG JAR_PATH='boot/build/libs/tp2intervals.jar' COPY $JAR_PATH /app/app.jar -COPY scripts /scripts ENTRYPOINT java -jar /app/app.jar diff --git a/README.md b/README.md index 95cf84af..e5161493 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,8 @@ Runs on MacOS (DMG), Windows (EXE installer), Linux (AppImage). Alternatively th All files are available for download on [Release page](https://github.com/freekode/tp2intervals/releases/latest). +**Only for educational purposes** + * [List of features](#list-of-features) @@ -23,38 +25,46 @@ All files are available for download on [Release page](https://github.com/freeko + [Docker](#docker) * [FAQ](#faq) + [General](#general) - + [Sync automatically planned workouts to TrainingPeaks](#sync-automatically-planned-workouts-to-trainingpeaks) + [Info regarding scheduling for the next day with TrainingPeaks free account](#info-regarding-scheduling-for-the-next-day-with-trainingpeaks-free-account) * [Troubleshooting](#troubleshooting) + [How to get logs](#how-to-get-logs) + [How to record HAR file](#how-to-record-har-file) -**TrainerRoad Updates ⚠️** + +**New Docker image location ⚠️** + +**New image url: `ghcr.io/freekode/tp2intervals`** + +Old image url: `ghcr.io/freekode/tp2intervals/tp2intervals` + +**TrainerRoad Updates** I don't have access to TrainerRoad anymore. Account, which I used, cancelled subscription. I don't use the platform and it's too expensive to have it for occasional fixes. To fix issues I can only relay on logs and HAR files from you. ## List of features -### TrainingPeaks features -**Athlete account** +**TrainingPeaks** + +Athlete account * Sync planned workouts in calendar between Intervals.icu and TrainingPeaks (for today and tomorrow with free TP account) * Copy whole training plan from TrainingPeaks * Create training plan or workout folder on Intervals.icu from planned workouts on TrainingPeaks -**Coach account** +Coach account * Copy whole training plan and workout library from TrainingPeaks -### TrainerRoad features +**TrainerRoad** * Sync planned workouts in calendar from TrainerRoad to TrainingPeaks or Intervals.icu * Copy workouts from TrainerRoad library to Intervals * Create training plan or workout folder on Intervals.icu from planned workouts on TrainerRoad -**Only for educational purposes** +Automatically schedule workouts for today, by checking your calendar every 20 minutes. +To clear up scheduled jobs just restart the application. -## Configuration +## Configuration Before using the application you need to configure access to platforms. Access to Intervals.icu is required, access to other platforms is optional. @@ -87,7 +97,6 @@ Cookie `SharedTrainerRoadAuth` (key and value, smth like `SharedTrainerRoadAuth= Be aware, Firefox cuts long strings in Dev Tool window. Copy cookie value with right click -> Copy Value. ## Other ways to run the app - ### Executable JAR The project has executable jar with web UI. It requires JDK 21. To run jar: ```shell @@ -102,19 +111,13 @@ java -Dserver.port=9090 -jar tp2intervals.jar ### Docker Docker image also built for every release -To run docker execute: - -```shell -docker run --rm --name tp2intervals -p 8080:8080 ghcr.io/freekode/tp2intervals/tp2intervals:latest -``` - -Or with `docker compose` - ```yaml services: app: - image: ghcr.io/freekode/tp2intervals/tp2intervals:latest + image: ghcr.io/freekode/tp2intervals:latest container_name: tp2intervals + volumes: + - ./tp2intervals.sqlite:/tp2intervals.sqlite ports: - '8080:8080' ``` @@ -125,32 +128,10 @@ services: * Ramp steps in TrainerRoad are not supported * **MacOS arm64** Error: `"tp2intervals" is damaged and can’t be opened.` Run command in terminal `xattr -d com.apple.quarantine /Applications/tp2intervals.app` and then open app again -* **MacOS** app is not signed. Usually you need to open it twice. After opening it, be patient, it takes some time to - start +* **MacOS** app is not signed. Usually you need to open it twice * **Windows** The app will ask to access local network and Internet, you need to allow it. After all it makes HTTP requests * More info you can find on the forum https://forum.intervals.icu/t/tp2intervals-copy-trainingpeaks-and-trainerroad-workouts-plans-to-intervals/63375 - -### Bash script to sync planned workouts -To sync workouts without clicking buttons on UI there is a script [sync-planned-workouts.sh](scripts/sync-planned-workouts.sh). - -```sh -./sync-planned-workouts.sh -``` - -Example, sync workouts from TrainerRoad to TrainingPeaks for tomorrow in standalone app: -```sh -./sync-planned-workouts.sh tomorrow TRAINER_ROAD TRAINING_PEAKS standalone -``` - -Example, sync workouts from Intervals.icu to TrainingPeaks for 2025-01-05 in docker: -```sh -./sync-planned-workouts.sh 2025-01-05 INTERVALS TRAINING_PEAKS -``` - -Docker image has build in cron, you can edit its configuration and add script to run it on schedule - - ### Info regarding scheduling for the next day with TrainingPeaks free account Officially if you have a free TP account, you can't plan workouts for future dates, but practically you can. You can plan a workout for the next day relative to TrainingPeaks server local time. The server is in UTC-6 time zone. Let's check some examples: diff --git a/boot/src/main/kotlin/org/freekode/tp2intervals/Application.kt b/boot/src/main/kotlin/org/freekode/tp2intervals/Application.kt index eb6a2abc..6b06d600 100644 --- a/boot/src/main/kotlin/org/freekode/tp2intervals/Application.kt +++ b/boot/src/main/kotlin/org/freekode/tp2intervals/Application.kt @@ -6,10 +6,12 @@ import org.springframework.boot.context.properties.EnableConfigurationProperties import org.springframework.boot.runApplication import org.springframework.cache.annotation.EnableCaching import org.springframework.cloud.openfeign.EnableFeignClients +import org.springframework.scheduling.annotation.EnableScheduling @SpringBootApplication @EnableFeignClients @EnableCaching +@EnableScheduling @EnableConfigurationProperties(DefaultConfiguration::class) class Application diff --git a/boot/src/main/kotlin/org/freekode/tp2intervals/app/confguration/ConfigurationService.kt b/boot/src/main/kotlin/org/freekode/tp2intervals/app/confguration/ConfigurationService.kt index 3b33ce35..1a0e1b7a 100644 --- a/boot/src/main/kotlin/org/freekode/tp2intervals/app/confguration/ConfigurationService.kt +++ b/boot/src/main/kotlin/org/freekode/tp2intervals/app/confguration/ConfigurationService.kt @@ -31,6 +31,9 @@ class ConfigurationService( return errors } + fun platformInfo() = + platformInfoRepositoryMap.entries.associate { it.key to it.value.platformInfo() } + fun platformInfo(platform: Platform): PlatformInfo { return platformInfoRepositoryMap[platform]!!.platformInfo() } diff --git a/boot/src/main/kotlin/org/freekode/tp2intervals/app/workout/CopyFromCalendarToCalendarRequest.kt b/boot/src/main/kotlin/org/freekode/tp2intervals/app/workout/CopyC2CRequest.kt similarity index 99% rename from boot/src/main/kotlin/org/freekode/tp2intervals/app/workout/CopyFromCalendarToCalendarRequest.kt rename to boot/src/main/kotlin/org/freekode/tp2intervals/app/workout/CopyC2CRequest.kt index 97391230..6c5c4983 100644 --- a/boot/src/main/kotlin/org/freekode/tp2intervals/app/workout/CopyFromCalendarToCalendarRequest.kt +++ b/boot/src/main/kotlin/org/freekode/tp2intervals/app/workout/CopyC2CRequest.kt @@ -11,4 +11,4 @@ data class CopyFromCalendarToCalendarRequest( val skipSynced: Boolean, val sourcePlatform: Platform, val targetPlatform: Platform -) +) \ No newline at end of file diff --git a/boot/src/main/kotlin/org/freekode/tp2intervals/app/workout/CopyFromCalendarToLibraryRequest.kt b/boot/src/main/kotlin/org/freekode/tp2intervals/app/workout/CopyC2LRequest.kt similarity index 100% rename from boot/src/main/kotlin/org/freekode/tp2intervals/app/workout/CopyFromCalendarToLibraryRequest.kt rename to boot/src/main/kotlin/org/freekode/tp2intervals/app/workout/CopyC2LRequest.kt diff --git a/boot/src/main/kotlin/org/freekode/tp2intervals/app/workout/CopyFromLibraryToLibraryRequest.kt b/boot/src/main/kotlin/org/freekode/tp2intervals/app/workout/CopyL2LRequest.kt similarity index 100% rename from boot/src/main/kotlin/org/freekode/tp2intervals/app/workout/CopyFromLibraryToLibraryRequest.kt rename to boot/src/main/kotlin/org/freekode/tp2intervals/app/workout/CopyL2LRequest.kt diff --git a/boot/src/main/kotlin/org/freekode/tp2intervals/app/workout/WorkoutService.kt b/boot/src/main/kotlin/org/freekode/tp2intervals/app/workout/WorkoutService.kt index 54f60c4b..1b3eb6e0 100644 --- a/boot/src/main/kotlin/org/freekode/tp2intervals/app/workout/WorkoutService.kt +++ b/boot/src/main/kotlin/org/freekode/tp2intervals/app/workout/WorkoutService.kt @@ -6,6 +6,7 @@ import org.freekode.tp2intervals.domain.librarycontainer.LibraryContainerReposit import org.freekode.tp2intervals.domain.workout.WorkoutDetails import org.freekode.tp2intervals.domain.workout.WorkoutRepository import org.freekode.tp2intervals.rest.workout.DeleteWorkoutRequestDTO +import org.slf4j.LoggerFactory import org.springframework.stereotype.Service import java.time.LocalDate @@ -14,10 +15,12 @@ class WorkoutService( workoutRepositories: List, planRepositories: List, ) { + private val log = LoggerFactory.getLogger(this.javaClass) private val workoutRepositoryMap = workoutRepositories.associateBy { it.platform() } private val planRepositoryMap = planRepositories.associateBy { it.platform() } - fun copyWorkoutsFromCalendarToCalendar(request: CopyFromCalendarToCalendarRequest): CopyWorkoutsResponse { + fun copyWorkoutsC2C(request: CopyFromCalendarToCalendarRequest): CopyWorkoutsResponse { + log.info("Received request for copy calendar to calendar: $request") val sourceWorkoutRepository = workoutRepositoryMap[request.sourcePlatform]!! val targetWorkoutRepository = workoutRepositoryMap[request.targetPlatform]!! @@ -36,10 +39,12 @@ class WorkoutService( ExternalData.empty() // TODO figure smth better ) targetWorkoutRepository.saveWorkoutsToCalendar(filteredWorkoutsToSync) + log.info("Saved workouts to calendar successfully: $response") return response } - fun copyWorkoutsFromCalendarToLibrary(request: CopyFromCalendarToLibraryRequest): CopyWorkoutsResponse { + fun copyWorkoutsC2L(request: CopyFromCalendarToLibraryRequest): CopyWorkoutsResponse { + log.info("Received request for copy calendar to library: $request") val sourceWorkoutRepository = workoutRepositoryMap[request.sourcePlatform]!! val targetWorkoutRepository = workoutRepositoryMap[request.targetPlatform]!! val targetPlanRepository = planRepositoryMap[request.targetPlatform]!! @@ -58,7 +63,8 @@ class WorkoutService( ) } - fun copyWorkoutFromLibraryToLibrary(request: CopyFromLibraryToLibraryRequest): CopyWorkoutsResponse { + fun copyWorkoutL2L(request: CopyFromLibraryToLibraryRequest): CopyWorkoutsResponse { + log.info("Received request for copy library to library: $request") val sourceWorkoutRepository = workoutRepositoryMap[request.sourcePlatform]!! val targetWorkoutRepository = workoutRepositoryMap[request.targetPlatform]!! @@ -68,10 +74,12 @@ class WorkoutService( } fun findWorkoutsByName(platform: Platform, name: String): List { + log.info("Received request for find workouts by name, platform: $platform, name: $name") return workoutRepositoryMap[platform]!!.findWorkoutsFromLibraryByName(name) } fun deleteWorkoutsFromCalendar(request: DeleteWorkoutRequestDTO) { + log.info("Received request to delete workouts from calendar: $request") val workoutRepository = workoutRepositoryMap[request.platform]!! workoutRepository.deleteWorkoutsFromCalendar(request.startDate, request.endDate) } diff --git a/boot/src/main/kotlin/org/freekode/tp2intervals/app/workout/schedule/C2CTodayScheduledRequest.kt b/boot/src/main/kotlin/org/freekode/tp2intervals/app/workout/schedule/C2CTodayScheduledRequest.kt new file mode 100644 index 00000000..74e92c09 --- /dev/null +++ b/boot/src/main/kotlin/org/freekode/tp2intervals/app/workout/schedule/C2CTodayScheduledRequest.kt @@ -0,0 +1,22 @@ +package org.freekode.tp2intervals.app.workout.schedule + +import org.freekode.tp2intervals.app.workout.CopyFromCalendarToCalendarRequest +import org.freekode.tp2intervals.domain.Platform +import org.freekode.tp2intervals.domain.TrainingType +import java.time.LocalDate + +data class C2CTodayScheduledRequest( + val types: List, + val skipSynced: Boolean, + val sourcePlatform: Platform, + val targetPlatform: Platform +) : Schedulable { + fun forToday() = CopyFromCalendarToCalendarRequest( + LocalDate.now(), + LocalDate.now(), + types, + skipSynced, + sourcePlatform, + targetPlatform + ) +} \ No newline at end of file diff --git a/boot/src/main/kotlin/org/freekode/tp2intervals/app/workout/schedule/Schedulable.kt b/boot/src/main/kotlin/org/freekode/tp2intervals/app/workout/schedule/Schedulable.kt new file mode 100644 index 00000000..3d99df24 --- /dev/null +++ b/boot/src/main/kotlin/org/freekode/tp2intervals/app/workout/schedule/Schedulable.kt @@ -0,0 +1,4 @@ +package org.freekode.tp2intervals.app.workout.schedule + + +interface Schedulable diff --git a/boot/src/main/kotlin/org/freekode/tp2intervals/app/workout/schedule/WorkoutScheduledJob.kt b/boot/src/main/kotlin/org/freekode/tp2intervals/app/workout/schedule/WorkoutScheduledJob.kt new file mode 100644 index 00000000..e310b107 --- /dev/null +++ b/boot/src/main/kotlin/org/freekode/tp2intervals/app/workout/schedule/WorkoutScheduledJob.kt @@ -0,0 +1,52 @@ +package org.freekode.tp2intervals.app.workout.schedule + +import com.fasterxml.jackson.databind.ObjectMapper +import org.freekode.tp2intervals.app.workout.WorkoutService +import org.freekode.tp2intervals.infrastructure.schedule.ScheduleRequestEntity +import org.freekode.tp2intervals.infrastructure.schedule.ScheduleRequestRepository +import org.slf4j.LoggerFactory +import org.springframework.scheduling.annotation.Scheduled +import org.springframework.stereotype.Service +import java.util.concurrent.TimeUnit + +@Service +class WorkoutScheduledJob( + private val workoutService: WorkoutService, + private val scheduleRequestRepository: ScheduleRequestRepository, + private val objectMapper: ObjectMapper +) { + private val log = LoggerFactory.getLogger(this.javaClass) + + fun addRequest(schedulable: Schedulable) { + val requestJson = objectMapper.writeValueAsString(schedulable) + if (scheduleRequestRepository.findByRequestJson(requestJson) != null) throw IllegalArgumentException("Request already exists") + scheduleRequestRepository.save(ScheduleRequestEntity(requestJson)) + } + + fun getRequests() = + scheduleRequestRepository.findAll().toList() + + fun deleteRequest(id: Int) { + scheduleRequestRepository.deleteById(id) + } + + @Scheduled(fixedRate = 3, timeUnit = TimeUnit.MINUTES) + fun job() { + val requests = getRequests().map { it.toSchedulable() } + log.info("Starting processing scheduled requests. There are ${requests.size} requests") + + for (request in requests) { + handleCopyCalendarToCalendarRequest(request) + } + + log.info("Finished processing scheduled requests"); + } + + private fun handleCopyCalendarToCalendarRequest(request: C2CTodayScheduledRequest) { + workoutService.copyWorkoutsC2C(request.forToday()) + } + + private fun ScheduleRequestEntity.toSchedulable(): C2CTodayScheduledRequest { + return objectMapper.readValue(requestJson, C2CTodayScheduledRequest::class.java) + } +} diff --git a/boot/src/main/kotlin/org/freekode/tp2intervals/domain/ExternalData.kt b/boot/src/main/kotlin/org/freekode/tp2intervals/domain/ExternalData.kt index 89d4fe4a..451fb56e 100644 --- a/boot/src/main/kotlin/org/freekode/tp2intervals/domain/ExternalData.kt +++ b/boot/src/main/kotlin/org/freekode/tp2intervals/domain/ExternalData.kt @@ -19,7 +19,7 @@ data class ExternalData( fun withTrainerRoad(trainerRoadId: String) = ExternalData(trainingPeaksId, intervalsId, trainerRoadId) - fun withSimpleString(string: String): ExternalData { + fun fromSimpleString(string: String): ExternalData { val split = string.split(externalDataDescriptionSeparator) if (split.size != 2) { return this diff --git a/boot/src/main/kotlin/org/freekode/tp2intervals/infrastructure/platform/intervalsicu/workout/FromIntervalsWorkoutConverter.kt b/boot/src/main/kotlin/org/freekode/tp2intervals/infrastructure/platform/intervalsicu/workout/FromIntervalsWorkoutConverter.kt index ce984998..63db0029 100644 --- a/boot/src/main/kotlin/org/freekode/tp2intervals/infrastructure/platform/intervalsicu/workout/FromIntervalsWorkoutConverter.kt +++ b/boot/src/main/kotlin/org/freekode/tp2intervals/infrastructure/platform/intervalsicu/workout/FromIntervalsWorkoutConverter.kt @@ -3,7 +3,11 @@ package org.freekode.tp2intervals.infrastructure.platform.intervalsicu.workout import org.freekode.tp2intervals.domain.ExternalData import org.freekode.tp2intervals.domain.workout.Workout import org.freekode.tp2intervals.domain.workout.WorkoutDetails -import org.freekode.tp2intervals.domain.workout.structure.* +import org.freekode.tp2intervals.domain.workout.structure.MultiStep +import org.freekode.tp2intervals.domain.workout.structure.SingleStep +import org.freekode.tp2intervals.domain.workout.structure.StepLength +import org.freekode.tp2intervals.domain.workout.structure.WorkoutStep +import org.freekode.tp2intervals.domain.workout.structure.WorkoutStructure class FromIntervalsWorkoutConverter( private val eventDTO: IntervalsEventDTO @@ -20,7 +24,7 @@ class FromIntervalsWorkoutConverter( eventDTO.description, eventDTO.mapDuration(), eventDTO.icu_training_load, - ExternalData.empty().withIntervals(eventDTO.id.toString()) + ExternalData.empty().withIntervals(eventDTO.id.toString()).fromSimpleString(eventDTO.description ?: "") ), eventDTO.start_date_local.toLocalDate(), workoutsStructure, diff --git a/boot/src/main/kotlin/org/freekode/tp2intervals/infrastructure/platform/intervalsicu/workout/ToIntervalsWorkoutConverter.kt b/boot/src/main/kotlin/org/freekode/tp2intervals/infrastructure/platform/intervalsicu/workout/ToIntervalsWorkoutConverter.kt index 23ebdd9a..82f3988f 100644 --- a/boot/src/main/kotlin/org/freekode/tp2intervals/infrastructure/platform/intervalsicu/workout/ToIntervalsWorkoutConverter.kt +++ b/boot/src/main/kotlin/org/freekode/tp2intervals/infrastructure/platform/intervalsicu/workout/ToIntervalsWorkoutConverter.kt @@ -1,10 +1,10 @@ package org.freekode.tp2intervals.infrastructure.platform.intervalsicu.workout -import java.time.LocalDate import org.freekode.tp2intervals.domain.librarycontainer.LibraryContainer import org.freekode.tp2intervals.domain.workout.Workout import org.freekode.tp2intervals.infrastructure.Signature import org.freekode.tp2intervals.infrastructure.utils.Date +import java.time.LocalDate class ToIntervalsWorkoutConverter { private val unwantedStepRegex = "^[-*]".toRegex(RegexOption.MULTILINE) @@ -53,6 +53,7 @@ class ToIntervalsWorkoutConverter { description += workoutString ?.let { "\n\n- - - -\n$it" } .orEmpty() + description += "\n\n${workout.details.externalData.toSimpleString()}" return description } diff --git a/boot/src/main/kotlin/org/freekode/tp2intervals/infrastructure/platform/trainingpeaks/workout/TPToWorkoutConverter.kt b/boot/src/main/kotlin/org/freekode/tp2intervals/infrastructure/platform/trainingpeaks/workout/TPToWorkoutConverter.kt index 7d4eae4a..f00d7a9e 100644 --- a/boot/src/main/kotlin/org/freekode/tp2intervals/infrastructure/platform/trainingpeaks/workout/TPToWorkoutConverter.kt +++ b/boot/src/main/kotlin/org/freekode/tp2intervals/infrastructure/platform/trainingpeaks/workout/TPToWorkoutConverter.kt @@ -65,6 +65,6 @@ class TPToWorkoutConverter { } private fun getWorkoutExternalData(tpWorkout: TPBaseWorkoutResponseDTO): ExternalData { - return ExternalData.empty().withTrainingPeaks(tpWorkout.id).withSimpleString(tpWorkout.description ?: "") + return ExternalData.empty().withTrainingPeaks(tpWorkout.id).fromSimpleString(tpWorkout.description ?: "") } } diff --git a/boot/src/main/kotlin/org/freekode/tp2intervals/infrastructure/schedule/ScheduleRequestEntity.kt b/boot/src/main/kotlin/org/freekode/tp2intervals/infrastructure/schedule/ScheduleRequestEntity.kt new file mode 100644 index 00000000..98a6474c --- /dev/null +++ b/boot/src/main/kotlin/org/freekode/tp2intervals/infrastructure/schedule/ScheduleRequestEntity.kt @@ -0,0 +1,22 @@ +package org.freekode.tp2intervals.infrastructure.schedule + +import jakarta.persistence.Column +import jakarta.persistence.Entity +import jakarta.persistence.GeneratedValue +import jakarta.persistence.GenerationType +import jakarta.persistence.Id +import jakarta.persistence.Table + +@Table(name = "schedule_requests") +@Entity +data class ScheduleRequestEntity( + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + val id: Int?, + + @Column + var requestJson: String?, +) { + constructor() : this(null, null) + constructor(requestJson: String) : this(null, requestJson) +} diff --git a/boot/src/main/kotlin/org/freekode/tp2intervals/infrastructure/schedule/ScheduleRequestRepository.kt b/boot/src/main/kotlin/org/freekode/tp2intervals/infrastructure/schedule/ScheduleRequestRepository.kt new file mode 100644 index 00000000..241de0b5 --- /dev/null +++ b/boot/src/main/kotlin/org/freekode/tp2intervals/infrastructure/schedule/ScheduleRequestRepository.kt @@ -0,0 +1,9 @@ +package org.freekode.tp2intervals.infrastructure.schedule + +import org.springframework.data.repository.CrudRepository +import org.springframework.stereotype.Repository + +@Repository +interface ScheduleRequestRepository : CrudRepository { + fun findByRequestJson(requestJson: String): ScheduleRequestEntity? +} diff --git a/boot/src/main/kotlin/org/freekode/tp2intervals/rest/configuration/ConfigurationController.kt b/boot/src/main/kotlin/org/freekode/tp2intervals/rest/configuration/ConfigurationController.kt index 298dcb59..6e1388cf 100644 --- a/boot/src/main/kotlin/org/freekode/tp2intervals/rest/configuration/ConfigurationController.kt +++ b/boot/src/main/kotlin/org/freekode/tp2intervals/rest/configuration/ConfigurationController.kt @@ -2,8 +2,6 @@ package org.freekode.tp2intervals.rest.configuration import org.freekode.tp2intervals.app.confguration.ConfigurationService import org.freekode.tp2intervals.domain.Platform -import org.freekode.tp2intervals.domain.TrainingType -import org.freekode.tp2intervals.domain.config.PlatformInfo import org.freekode.tp2intervals.domain.config.UpdateConfigurationRequest import org.freekode.tp2intervals.domain.workout.structure.StepModifier import org.freekode.tp2intervals.rest.ErrorResponseDTO @@ -23,14 +21,14 @@ class ConfigurationController( @GetMapping("/api/configuration") fun getConfigurations(): AppConfigurationDTO { - log.info("Received request for getting all configurations") + log.debug("Received request for getting all configurations") val configurations = configurationService.getConfigurations() return AppConfigurationDTO(configurations.configMap) } @PutMapping("/api/configuration") fun updateConfiguration(@RequestBody requestDTO: UpdateConfigurationRequestDTO): ResponseEntity { - log.info("Received request for updating configuration: $requestDTO") + log.debug("Received request for updating configuration: {}", requestDTO) val errors = configurationService.updateConfiguration(UpdateConfigurationRequest(requestDTO.config)) if (errors.isNotEmpty()) { return ResponseEntity.badRequest().body(ErrorResponseDTO(errors.joinToString())) @@ -38,20 +36,16 @@ class ConfigurationController( return ResponseEntity.ok().build() } - @Deprecated("all config on ui") - @GetMapping("/api/configuration/training-types") - fun getTrainingTypes(): List { - return TrainingType.entries.map { TrainingTypeDTO(it) } - } - @GetMapping("/api/configuration/intervals-step-modifiers") fun getIntervalsStepModifiers(): List { return StepModifier.entries } + @GetMapping("/api/configuration/platform") + fun getAllPlatformInfo() = + configurationService.platformInfo() + @GetMapping("/api/configuration/{platform}") - fun getConfigurations(@PathVariable platform: Platform): PlatformInfo { - log.info("Received request for getting configurations for platform: $platform") - return configurationService.platformInfo(platform) - } + fun getConfigurations(@PathVariable platform: Platform) = + configurationService.platformInfo(platform) } diff --git a/boot/src/main/kotlin/org/freekode/tp2intervals/rest/library/LibraryController.kt b/boot/src/main/kotlin/org/freekode/tp2intervals/rest/library/LibraryController.kt index b8f6a8e9..379afbe8 100644 --- a/boot/src/main/kotlin/org/freekode/tp2intervals/rest/library/LibraryController.kt +++ b/boot/src/main/kotlin/org/freekode/tp2intervals/rest/library/LibraryController.kt @@ -20,13 +20,13 @@ class LibraryController( @GetMapping("/api/library-container") fun getLibraryContainers(@RequestParam platform: Platform): List { - log.info("Received request for getting library containers: $platform") + log.debug("Received request for getting library containers: {}", platform) return libraryService.findByPlatform(platform) } @PostMapping("/api/library-container/copy") fun copyLibraryContainer(@RequestBody request: CopyLibraryRequest): CopyPlanResponse { - log.info("Received request to copy the library container: $request") + log.debug("Received request to copy the library container: {}", request) return libraryService.copyLibrary(request) } } diff --git a/boot/src/main/kotlin/org/freekode/tp2intervals/rest/workout/WorkoutController.kt b/boot/src/main/kotlin/org/freekode/tp2intervals/rest/workout/WorkoutController.kt index b73f3e3e..01fdb40b 100644 --- a/boot/src/main/kotlin/org/freekode/tp2intervals/rest/workout/WorkoutController.kt +++ b/boot/src/main/kotlin/org/freekode/tp2intervals/rest/workout/WorkoutController.kt @@ -6,7 +6,6 @@ import org.freekode.tp2intervals.app.workout.CopyFromLibraryToLibraryRequest import org.freekode.tp2intervals.app.workout.CopyWorkoutsResponse import org.freekode.tp2intervals.app.workout.WorkoutService import org.freekode.tp2intervals.domain.Platform -import org.slf4j.LoggerFactory import org.springframework.web.bind.annotation.DeleteMapping import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.PostMapping @@ -16,31 +15,25 @@ import org.springframework.web.bind.annotation.RestController @RestController class WorkoutController( - private val workoutService: WorkoutService + private val workoutService: WorkoutService, ) { - private val log = LoggerFactory.getLogger(this.javaClass) - @PostMapping("/api/workout/copy-calendar-to-calendar") fun copyWorkoutsFromCalendarToCalendar(@RequestBody request: CopyFromCalendarToCalendarRequest): CopyWorkoutsResponse { - log.info("Received request for copy calendar to calendar: $request") - return workoutService.copyWorkoutsFromCalendarToCalendar(request) + return workoutService.copyWorkoutsC2C(request) } @PostMapping("/api/workout/copy-calendar-to-library") fun copyWorkoutsFromCalendarToLibrary(@RequestBody request: CopyFromCalendarToLibraryRequest): CopyWorkoutsResponse { - log.info("Received request for copy calendar to library: $request") - return workoutService.copyWorkoutsFromCalendarToLibrary(request) + return workoutService.copyWorkoutsC2L(request) } @PostMapping("/api/workout/copy-library-to-library") fun copyWorkoutFromLibraryToLibrary(@RequestBody request: CopyFromLibraryToLibraryRequest): CopyWorkoutsResponse { - log.info("Received request for copy library to library: $request") - return workoutService.copyWorkoutFromLibraryToLibrary(request) + return workoutService.copyWorkoutL2L(request) } @GetMapping("/api/workout/find") fun findWorkoutsByName(@RequestParam platform: Platform, @RequestParam name: String): List { - log.info("Received request for find workouts by name, platform: $platform, name: $name") return workoutService.findWorkoutsByName(platform, name) .map { workoutDetails -> WorkoutDetailsDTO( @@ -54,7 +47,6 @@ class WorkoutController( @DeleteMapping("/api/workout") fun deleteWorkoutsFromCalendar(@RequestBody request: DeleteWorkoutRequestDTO) { - log.info("Received request to delete workouts from calendar: $request") workoutService.deleteWorkoutsFromCalendar(request) } } diff --git a/boot/src/main/kotlin/org/freekode/tp2intervals/rest/workout/WorkoutScheduledJobController.kt b/boot/src/main/kotlin/org/freekode/tp2intervals/rest/workout/WorkoutScheduledJobController.kt new file mode 100644 index 00000000..7fb0fd14 --- /dev/null +++ b/boot/src/main/kotlin/org/freekode/tp2intervals/rest/workout/WorkoutScheduledJobController.kt @@ -0,0 +1,28 @@ +package org.freekode.tp2intervals.rest.workout + +import org.freekode.tp2intervals.app.workout.schedule.C2CTodayScheduledRequest +import org.freekode.tp2intervals.app.workout.schedule.WorkoutScheduledJob +import org.springframework.web.bind.annotation.DeleteMapping +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RestController + +@RestController +class WorkoutScheduledJobController( + private val workoutScheduledJob: WorkoutScheduledJob +) { + @PostMapping("/api/workout/copy-calendar-to-calendar/schedule") + fun scheduleC2CTodayRequest(@RequestBody request: C2CTodayScheduledRequest) { + workoutScheduledJob.addRequest(request) + } + + @GetMapping("/api/workout/copy-calendar-to-calendar/schedule") + fun getScheduleRequests() = + workoutScheduledJob.getRequests() + + @DeleteMapping("/api/workout/copy-calendar-to-calendar/schedule/{id}") + fun deleteScheduleRequest(@PathVariable id: Int) = + workoutScheduledJob.deleteRequest(id) +} diff --git a/boot/src/main/resources/db/changelog/schemas/0008-add-scheduled-requests.yaml b/boot/src/main/resources/db/changelog/schemas/0008-add-scheduled-requests.yaml new file mode 100644 index 00000000..95bf1686 --- /dev/null +++ b/boot/src/main/resources/db/changelog/schemas/0008-add-scheduled-requests.yaml @@ -0,0 +1,23 @@ +databaseChangeLog: + - changeSet: + id: _ + author: _ + changes: + - dropTable: + tableName: schedule_requests + - createTable: + tableName: schedule_requests + columns: + - column: + name: id + type: int + autoIncrement: true + constraints: + primaryKey: true + unique: true + + - column: + name: request_json + type: varchar(5000) + constraints: + nullable: false diff --git a/boot/src/main/resources/ehcache.xml b/boot/src/main/resources/ehcache.xml index 2b465a65..5647f253 100644 --- a/boot/src/main/resources/ehcache.xml +++ b/boot/src/main/resources/ehcache.xml @@ -6,7 +6,7 @@ java.lang.String java.lang.String - 1 + 50 1 diff --git a/boot/src/test/kotlin/org/freekode/tp2intervals/app/workout/TrainerRoadWorkoutServiceIT.kt b/boot/src/test/kotlin/org/freekode/tp2intervals/app/workout/TrainerRoadWorkoutServiceIT.kt index 6e465e7e..2a10c6a0 100644 --- a/boot/src/test/kotlin/org/freekode/tp2intervals/app/workout/TrainerRoadWorkoutServiceIT.kt +++ b/boot/src/test/kotlin/org/freekode/tp2intervals/app/workout/TrainerRoadWorkoutServiceIT.kt @@ -38,7 +38,7 @@ class TrainerRoadWorkoutServiceIT : BaseSpringITConfig() { platform, Platform.INTERVALS ) - val response = workoutService.copyWorkoutFromLibraryToLibrary(copyRequest) + val response = workoutService.copyWorkoutL2L(copyRequest) libraryService.deleteLibrary(DeleteLibraryRequest(libraryContainer.externalData, Platform.INTERVALS)) assertEquals(response.copied, 1) } @@ -46,7 +46,7 @@ class TrainerRoadWorkoutServiceIT : BaseSpringITConfig() { @Test @Disabled("don't have example response for calendar") fun `should copy planned workouts to library`() { - val response = workoutService.copyWorkoutsFromCalendarToLibrary( + val response = workoutService.copyWorkoutsC2L( CopyFromCalendarToLibraryRequest( LocalDate.parse("2024-03-04"), LocalDate.parse("2024-03-10"), diff --git a/boot/src/test/kotlin/org/freekode/tp2intervals/app/workout/TrainingPeaksWorkoutServiceIT.kt b/boot/src/test/kotlin/org/freekode/tp2intervals/app/workout/TrainingPeaksWorkoutServiceIT.kt index 5a2ab0c2..4f10d188 100644 --- a/boot/src/test/kotlin/org/freekode/tp2intervals/app/workout/TrainingPeaksWorkoutServiceIT.kt +++ b/boot/src/test/kotlin/org/freekode/tp2intervals/app/workout/TrainingPeaksWorkoutServiceIT.kt @@ -32,12 +32,12 @@ class TrainingPeaksWorkoutServiceIT : BaseSpringITConfig() { val copyRequest = CopyFromCalendarToCalendarRequest( startDate, endDate, - TrainingType.Companion.DEFAULT_LIST, + TrainingType.DEFAULT_LIST, true, Platform.INTERVALS, platform ) - val response = workoutService.copyWorkoutsFromCalendarToCalendar(copyRequest) + val response = workoutService.copyWorkoutsC2C(copyRequest) workoutService.deleteWorkoutsFromCalendar(deleteRequest) @@ -65,7 +65,7 @@ class TrainingPeaksWorkoutServiceIT : BaseSpringITConfig() { @Test fun `should copy planned workouts to library`() { - val response = workoutService.copyWorkoutsFromCalendarToLibrary( + val response = workoutService.copyWorkoutsC2L( CopyFromCalendarToLibraryRequest( LocalDate.parse("2024-03-04"), LocalDate.parse("2024-03-10"), diff --git a/boot/src/test/kotlin/org/freekode/tp2intervals/app/workout/WorkoutJobSchedulerIT.kt b/boot/src/test/kotlin/org/freekode/tp2intervals/app/workout/WorkoutJobSchedulerIT.kt new file mode 100644 index 00000000..0c77af31 --- /dev/null +++ b/boot/src/test/kotlin/org/freekode/tp2intervals/app/workout/WorkoutJobSchedulerIT.kt @@ -0,0 +1,28 @@ +package org.freekode.tp2intervals.app.workout + +import config.BaseSpringITConfig +import org.assertj.core.api.Assertions.assertThat +import org.freekode.tp2intervals.app.workout.schedule.C2CTodayScheduledRequest +import org.freekode.tp2intervals.app.workout.schedule.WorkoutScheduledJob +import org.freekode.tp2intervals.domain.Platform +import org.freekode.tp2intervals.domain.TrainingType +import org.freekode.tp2intervals.infrastructure.schedule.ScheduleRequestEntity +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired + +class WorkoutJobSchedulerIT : BaseSpringITConfig() { + @Autowired + lateinit var workoutScheduledJob: WorkoutScheduledJob + + @Test + fun test() { + val request = + C2CTodayScheduledRequest(listOf(TrainingType.BIKE), true, Platform.INTERVALS, Platform.TRAINING_PEAKS) + workoutScheduledJob.addRequest(request) + + val requests = workoutScheduledJob.getRequests() + + assertThat(requests.isNotEmpty()).isTrue() + assertThat(requests[0] is ScheduleRequestEntity).isTrue() + } +} diff --git a/boot/src/test/kotlin/org/freekode/tp2intervals/domain/ExternalDataTest.kt b/boot/src/test/kotlin/org/freekode/tp2intervals/domain/ExternalDataTest.kt index a50a14a8..181e70cc 100644 --- a/boot/src/test/kotlin/org/freekode/tp2intervals/domain/ExternalDataTest.kt +++ b/boot/src/test/kotlin/org/freekode/tp2intervals/domain/ExternalDataTest.kt @@ -11,7 +11,7 @@ class ExternalDataTest { // when var data = ExternalData(null, null, null) - data = data.withSimpleString(string) + data = data.fromSimpleString(string) // then Assertions.assertNotNull(data) @@ -28,9 +28,9 @@ class ExternalDataTest { // when var data = ExternalData(null, null, null) - data = data.withSimpleString(string) + data = data.fromSimpleString(string) var data1 = ExternalData(null, null, null) - data1 = data1.withSimpleString(string1) + data1 = data1.fromSimpleString(string1) // then Assertions.assertNull(data.trainingPeaksId) diff --git a/boot/version b/boot/version index 78bc1abd..26acbf08 100644 --- a/boot/version +++ b/boot/version @@ -1 +1 @@ -0.10.0 +0.12.2 diff --git a/docker-compose.yml b/docker-compose.yml index c61e1bb3..3852229e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,9 +1,9 @@ -version: '3.1' - services: app: - image: ghcr.io/freekode/tp2intervals/tp2intervals:latest + image: ghcr.io/freekode/tp2intervals:latest container_name: tp2intervals restart: unless-stopped + volumes: + - ./tp2intervals.sqlite:/tp2intervals.sqlite ports: - '8080:8080' diff --git a/electron/package-lock.json b/electron/package-lock.json index fb1c64e1..ee376643 100644 --- a/electron/package-lock.json +++ b/electron/package-lock.json @@ -1,12 +1,12 @@ { "name": "tp2intervals", - "version": "0.10.0", + "version": "0.12.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "tp2intervals", - "version": "0.10.0", + "version": "0.12.2", "license": "GNU GPLv3", "dependencies": { "electron-log": "^5.1.1", diff --git a/electron/package.json b/electron/package.json index eb6114b8..dd5618c9 100644 --- a/electron/package.json +++ b/electron/package.json @@ -1,7 +1,7 @@ { "name": "tp2intervals", "productName": "tp2intervals", - "version": "0.10.0", + "version": "0.12.2", "description": "Third Party synchronization with Intervals.icu", "keywords": [ "trainingpeaks", diff --git a/scripts/sync-planned-workouts.sh b/scripts/sync-planned-workouts.sh deleted file mode 100644 index ffdced15..00000000 --- a/scripts/sync-planned-workouts.sh +++ /dev/null @@ -1,42 +0,0 @@ -#!/bin/sh - -# How to run -# ./sync-planned-workouts.sh (tomorrow|) (INTERVALS|TRAINING_PEAKS|TRAINER_ROAD) (INTERVALS|TRAINING_PEAKS|TRAINER_ROAD) [standalone] -# -# Examples: -# ./sync-planned-workouts.sh tomorrow TRAINER_ROAD TRAINING_PEAKS standalone -# ./sync-planned-workouts.sh 2025-01-05 INTERVALS TRAINING_PEAKS - - -syncDate= -sourcePlatform=$2 # INTERVALS, TRAINING_PEAKS, TRAINER_ROAD -targetPlatform=$3 # INTERVALS, TRAINING_PEAKS, TRAINER_ROAD -port=8080 # 8080, 44864 - - -tomorrow_date() { - platform=$(uname) - if [ "$platform" == "Darwin" ]; then - echo $(date -v+1d +"%Y-%m-%d") - elif [ "$platform" == "Linux" ]; then - echo $(date -d @$(( $(date +%s) + 86400 )) +%F) - fi - return 0 -} - -if [ "$1" = "tomorrow" ]; then - syncDate=$(tomorrow_date) -else - syncDate=$1 -fi - -if [ "$4" = "standalone" ]; then - port=44864 -fi - -postData='{"startDate":"'$syncDate'","endDate":"'$syncDate'","types":["BIKE","VIRTUAL_BIKE","MTB","RUN"],"skipSynced":true,"sourcePlatform":"'$sourcePlatform'","targetPlatform":"'targetPlatform'"}' - -wget -O - \ - --header="Content-Type: application/json" \ - --post-data=$postData \ - http://localhost:$port/api/workout/copy-calendar-to-calendar diff --git a/ui/src/app/components/copy-calendar-to-calendar/copy-calendar-to-calendar.component.html b/ui/src/app/components/copy-calendar-to-calendar/copy-calendar-to-calendar.component.html new file mode 100644 index 00000000..6c870b63 --- /dev/null +++ b/ui/src/app/components/copy-calendar-to-calendar/copy-calendar-to-calendar.component.html @@ -0,0 +1,121 @@ +
+
+ + Direction + + @for (item of directions; track item) { + {{ item.title }} + } + + +
+ +
+ + Workout Types + + @for (type of trainingTypes; track type) { + {{ type.title }} + } + + +
+ +
+ + Date Range + + + + + + + + + +
+ +
+ + Skip already synced workouts + +
+ +
+ + + +
+ +
+ + + Scheduled jobs + + + @for (request of scheduleRequests; track request) { + + + Types: {{ mapTrainingTypesToTitles(request.request.types) }}, + Skip synced: {{ request.request.skipSynced }}, + {{ Platform.getTitle(request.request.sourcePlatform) }} -> {{ Platform.getTitle(request.request.targetPlatform) }} + + } + +
+ +
+ + +
+ + +
diff --git a/ui/src/app/components/copy-calendar-to-calendar/copy-calendar-to-calendar.component.scss b/ui/src/app/components/copy-calendar-to-calendar/copy-calendar-to-calendar.component.scss new file mode 100644 index 00000000..1b1e613c --- /dev/null +++ b/ui/src/app/components/copy-calendar-to-calendar/copy-calendar-to-calendar.component.scss @@ -0,0 +1,5 @@ +.date-range-text { + padding-top: 10px; + padding-bottom: 10px; + padding-left: 10px; +} diff --git a/ui/src/app/components/copy-calendar-to-calendar/copy-calendar-to-calendar.component.ts b/ui/src/app/components/copy-calendar-to-calendar/copy-calendar-to-calendar.component.ts new file mode 100644 index 00000000..e875c2e6 --- /dev/null +++ b/ui/src/app/components/copy-calendar-to-calendar/copy-calendar-to-calendar.component.ts @@ -0,0 +1,160 @@ +import {Component, Input, OnInit} from '@angular/core'; +import {FormBuilder, FormGroup, FormsModule, ReactiveFormsModule, Validators} from "@angular/forms"; +import {MatButtonModule} from "@angular/material/button"; +import {MatFormFieldModule} from "@angular/material/form-field"; +import {MatInputModule} from "@angular/material/input"; +import {MatProgressBarModule} from "@angular/material/progress-bar"; +import {MatDatepickerModule} from "@angular/material/datepicker"; +import {MatNativeDateModule} from "@angular/material/core"; +import {MatSnackBarModule} from "@angular/material/snack-bar"; +import {MatSelectModule} from "@angular/material/select"; +import {MatCheckboxModule} from "@angular/material/checkbox"; +import {Platform} from "infrastructure/platform"; +import {formatDate} from "utils/date-formatter"; +import {MatDividerModule} from "@angular/material/divider"; +import {MatListModule} from "@angular/material/list"; +import {NgIf} from "@angular/common"; +import {ConfigurationClient} from "infrastructure/client/configuration.client"; +import {finalize, switchMap, tap} from "rxjs"; +import {WorkoutClient} from "infrastructure/client/workout.client"; +import {NotificationService} from "infrastructure/notification.service"; +import {MatTooltipModule} from "@angular/material/tooltip"; +import {TrainingTypes} from "infrastructure/training-types"; + +@Component({ + selector: 'copy-calendar-to-calendar', + standalone: true, + imports: [ + FormsModule, + MatButtonModule, + MatFormFieldModule, + MatInputModule, + ReactiveFormsModule, + MatProgressBarModule, + MatDatepickerModule, + MatNativeDateModule, + MatSnackBarModule, + MatSelectModule, + MatCheckboxModule, + MatDividerModule, + MatListModule, + NgIf, + MatTooltipModule, + ], + templateUrl: './copy-calendar-to-calendar.component.html', + styleUrl: './copy-calendar-to-calendar.component.scss' +}) +export class CopyCalendarToCalendarComponent implements OnInit { + readonly Platform = Platform; + readonly todayDate = new Date() + readonly tomorrowDate = new Date(new Date().getTime() + 24 * 60 * 60 * 1000) + + @Input() trainingTypes: any[] = [] + @Input() selectedTrainingTypes = ['BIKE', 'VIRTUAL_BIKE'] + @Input() directions: any[] = [] + @Input() inProgress = false + + formGroup: FormGroup + platformsInfo: any + scheduleRequests: any[] = [] + + constructor( + private formBuilder: FormBuilder, + private configurationClient: ConfigurationClient, + private workoutClient: WorkoutClient, + private notificationService: NotificationService + ) { + } + + ngOnInit(): void { + this.configurationClient.getAllPlatformInfo().subscribe(value => { + this.platformsInfo = value + }) + this.formGroup = this.getFormGroup(); + this.loadScheduleRequests().subscribe() + } + + submit() { + let startDate = formatDate(this.formGroup.controls['startDate'].value) + let endDate = formatDate(this.formGroup.controls['endDate'].value) + this.copyWorkouts(startDate, endDate); + } + + today() { + this.copyWorkoutsForOneDay(formatDate(this.todayDate)); + } + + tomorrow() { + this.copyWorkoutsForOneDay(formatDate(this.tomorrowDate)); + } + + scheduleToday() { + let startDate = null + let endDate = null + let direction = this.formGroup.value.direction + let trainingTypes = this.formGroup.value.trainingTypes + let skipSynced = this.formGroup.value.skipSynced + + this.inProgress = true + this.workoutClient.scheduleCopyCalendarToCalendar(startDate, endDate, trainingTypes, skipSynced, direction).pipe( + switchMap(() => this.loadScheduleRequests()), + finalize(() => this.inProgress = false) + ).subscribe(() => { + this.notificationService.success(`Scheduled sync job`) + }) + } + + mapTrainingTypesToTitles(values) { + return values.map(value => TrainingTypes.getTitle(value)) + } + + private copyWorkoutsForOneDay(date) { + this.copyWorkouts(date, date) + } + + private copyWorkouts(startDate, endDate) { + let direction = this.formGroup.value.direction + let trainingTypes = this.formGroup.value.trainingTypes + let skipSynced = this.formGroup.value.skipSynced + + this.inProgress = true + this.workoutClient.copyCalendarToCalendar(startDate, endDate, trainingTypes, skipSynced, direction).pipe( + finalize(() => this.inProgress = false) + ).subscribe((response) => { + this.notificationService.success( + `Planned: ${response.copied}\n Filtered out: ${response.filteredOut}\n From ${response.startDate} to ${response.endDate}`) + }) + } + + private getFormGroup() { + return this.formBuilder.group({ + direction: [this.directions[0].value, Validators.required], + trainingTypes: [this.selectedTrainingTypes, Validators.required], + startDate: [this.todayDate, Validators.required], + endDate: [this.tomorrowDate, Validators.required], + skipSynced: [true, Validators.required], + }) + } + + private loadScheduleRequests() { + return this.workoutClient.getScheduleRequests().pipe( + tap(values => { + this.scheduleRequests = values.map(value => { + return {id: value.id, request: JSON.parse(value.requestJson)} + }) + console.log(this.scheduleRequests) + } + ) + ) + } + + deleteJob(jobId: any) { + this.inProgress = true + this.workoutClient.deleteScheduleRequest(jobId).pipe( + switchMap(() => this.loadScheduleRequests()), + finalize(() => this.inProgress = false) + ).subscribe(() => { + this.notificationService.success(`Deleted job`) + }) + } +} diff --git a/ui/src/app/trainer-road/tr-copy-calendar-to-calendar/tr-copy-calendar-to-calendar.component.html b/ui/src/app/trainer-road/tr-copy-calendar-to-calendar/tr-copy-calendar-to-calendar.component.html index c1f14f3b..9911d0a4 100644 --- a/ui/src/app/trainer-road/tr-copy-calendar-to-calendar/tr-copy-calendar-to-calendar.component.html +++ b/ui/src/app/trainer-road/tr-copy-calendar-to-calendar/tr-copy-calendar-to-calendar.component.html @@ -1,78 +1,4 @@ -
-
- - Direction - - @for (item of directions; track item) { - {{ item.title }} - } - - -
- -
- - Workout Types - - @for (type of trainingTypes; track type) { - {{ type.title }} - } - - -
- -
- - Date Range - - - - - - - -
- -
- - Skip already synced workouts - -
- -
- - - -
- - -
+ + diff --git a/ui/src/app/trainer-road/tr-copy-calendar-to-calendar/tr-copy-calendar-to-calendar.component.scss b/ui/src/app/trainer-road/tr-copy-calendar-to-calendar/tr-copy-calendar-to-calendar.component.scss index 1b1e613c..e69de29b 100644 --- a/ui/src/app/trainer-road/tr-copy-calendar-to-calendar/tr-copy-calendar-to-calendar.component.scss +++ b/ui/src/app/trainer-road/tr-copy-calendar-to-calendar/tr-copy-calendar-to-calendar.component.scss @@ -1,5 +0,0 @@ -.date-range-text { - padding-top: 10px; - padding-bottom: 10px; - padding-left: 10px; -} diff --git a/ui/src/app/trainer-road/tr-copy-calendar-to-calendar/tr-copy-calendar-to-calendar.component.ts b/ui/src/app/trainer-road/tr-copy-calendar-to-calendar/tr-copy-calendar-to-calendar.component.ts index d8b98b2b..67e7cde6 100644 --- a/ui/src/app/trainer-road/tr-copy-calendar-to-calendar/tr-copy-calendar-to-calendar.component.ts +++ b/ui/src/app/trainer-road/tr-copy-calendar-to-calendar/tr-copy-calendar-to-calendar.component.ts @@ -1,23 +1,20 @@ import {Component, OnInit} from '@angular/core'; -import {FormBuilder, FormGroup, FormsModule, ReactiveFormsModule, Validators} from "@angular/forms"; +import {FormsModule, ReactiveFormsModule} from "@angular/forms"; import {MatButtonModule} from "@angular/material/button"; import {MatFormFieldModule} from "@angular/material/form-field"; import {MatInputModule} from "@angular/material/input"; import {MatProgressBarModule} from "@angular/material/progress-bar"; -import {NgIf} from "@angular/common"; import {MatDatepickerModule} from "@angular/material/datepicker"; import {MatNativeDateModule} from "@angular/material/core"; import {MatSnackBarModule} from "@angular/material/snack-bar"; import {MatSelectModule} from "@angular/material/select"; import {MatCheckboxModule} from "@angular/material/checkbox"; -import {WorkoutClient} from "infrastructure/client/workout.client"; -import {NotificationService} from "infrastructure/notification.service"; -import {finalize} from "rxjs"; import {Platform} from "infrastructure/platform"; -import {formatDate} from "utils/date-formatter"; -import {TrainingPeaksTrainingTypes} from "app/training-peaks/training-peaks-training-types"; -import {TrainerRoadTrainingTypes} from "app/trainer-road/trainer-road-training-types"; -import {ConfigurationClient} from "infrastructure/client/configuration.client"; +import {MatDividerModule} from "@angular/material/divider"; +import {MatListModule} from "@angular/material/list"; +import { + CopyCalendarToCalendarComponent +} from "app/components/copy-calendar-to-calendar/copy-calendar-to-calendar.component"; @Component({ selector: 'tr-copy-calendar-to-calendar', @@ -29,94 +26,33 @@ import {ConfigurationClient} from "infrastructure/client/configuration.client"; MatInputModule, ReactiveFormsModule, MatProgressBarModule, - NgIf, MatDatepickerModule, MatNativeDateModule, MatSnackBarModule, MatSelectModule, MatCheckboxModule, + MatDividerModule, + MatListModule, + CopyCalendarToCalendarComponent, ], templateUrl: './tr-copy-calendar-to-calendar.component.html', styleUrl: './tr-copy-calendar-to-calendar.component.scss' }) export class TrCopyCalendarToCalendarComponent implements OnInit { - readonly trainingTypes = TrainerRoadTrainingTypes.trainingTypes; - readonly todayDate = new Date() - readonly tomorrowDate = new Date(new Date().getTime() + 24 * 60 * 60 * 1000) + readonly Platform = Platform; readonly directions = [ {title: "TrainerRoad -> TrainingPeaks", value: Platform.DIRECTION_TR_TP}, {title: "TrainerRoad -> Intervals.icu", value: Platform.DIRECTION_TR_INT}, ] - readonly selectedTrainingTypes = ['BIKE', 'VIRTUAL_BIKE']; + readonly trainingTypes = [ + {title: "Ride", value: "BIKE"}, + {title: "Virtual Ride", value: "VIRTUAL_BIKE"}, + {title: "Unknown", value: "UNKNOWN"}, + ]; - formGroup: FormGroup = this.formBuilder.group({ - direction: [this.directions[0].value, Validators.required], - trainingTypes: [this.selectedTrainingTypes, Validators.required], - startDate: [this.todayDate, Validators.required], - endDate: [this.tomorrowDate, Validators.required], - skipSynced: [true, Validators.required], - }); - - inProgress = false - tpPlatformInfo: any = null - - constructor( - private formBuilder: FormBuilder, - private workoutClient: WorkoutClient, - private configurationClient: ConfigurationClient, - private notificationService: NotificationService - ) { + constructor() { } ngOnInit(): void { - this.configurationClient.platformInfo(Platform.TRAINING_PEAKS.key).subscribe(value => { - this.tpPlatformInfo = value - }) - this.listenDirectionChange(); - } - - submit() { - let startDate = formatDate(this.formGroup.controls['startDate'].value) - let endDate = formatDate(this.formGroup.controls['endDate'].value) - this.copyWorkouts(startDate, endDate); - } - - today() { - this.copyWorkoutsForOneDay(formatDate(this.todayDate)); - } - - tomorrow() { - this.copyWorkoutsForOneDay(formatDate(this.tomorrowDate)); } - - private copyWorkoutsForOneDay(date) { - this.copyWorkouts(date, date) - } - - private copyWorkouts(startDate, endDate) { - this.inProgress = true - let direction = this.formGroup.value.direction - let trainingTypes = this.formGroup.value.trainingTypes - let skipSynced = this.formGroup.value.skipSynced - this.workoutClient.copyCalendarToCalendar(startDate, endDate, trainingTypes, skipSynced, direction).pipe( - finalize(() => this.inProgress = false) - ).subscribe((response) => { - this.notificationService.success( - `Planned: ${response.copied}\n Filtered out: ${response.filteredOut}\n From ${response.startDate} to ${response.endDate}`) - }) - } - - private listenDirectionChange() { - this.formGroup.controls['direction'].valueChanges.subscribe(value => { - if (value === Platform.DIRECTION_TR_INT) { - this.formGroup.controls['skipSynced'].disable() - this.formGroup.controls['skipSynced'].setValue(false) - } else { - this.formGroup.controls['skipSynced'].enable() - this.formGroup.controls['skipSynced'].setValue(true) - } - }) - } - - protected readonly Platform = Platform; } diff --git a/ui/src/app/trainer-road/tr-copy-calendar-to-library/tr-copy-calendar-to-library.component.ts b/ui/src/app/trainer-road/tr-copy-calendar-to-library/tr-copy-calendar-to-library.component.ts index 83013a93..b4f7f01c 100644 --- a/ui/src/app/trainer-road/tr-copy-calendar-to-library/tr-copy-calendar-to-library.component.ts +++ b/ui/src/app/trainer-road/tr-copy-calendar-to-library/tr-copy-calendar-to-library.component.ts @@ -16,7 +16,6 @@ import {MatSnackBarModule} from "@angular/material/snack-bar"; import {MatSelectModule} from "@angular/material/select"; import {MatCheckboxModule} from "@angular/material/checkbox"; import {Platform} from "infrastructure/platform"; -import {TrainerRoadTrainingTypes} from "app/trainer-road/trainer-road-training-types"; @Component({ selector: 'tr-copy-calendar-to-library', @@ -40,8 +39,18 @@ import {TrainerRoadTrainingTypes} from "app/trainer-road/trainer-road-training-t styleUrl: './tr-copy-calendar-to-library.component.scss' }) export class TrCopyCalendarToLibraryComponent implements OnInit { - private readonly selectedTrainingTypes = ['BIKE', 'VIRTUAL_BIKE', 'MTB', 'RUN']; - private readonly direction = Platform.DIRECTION_TR_INT + readonly selectedTrainingTypes = ['BIKE', 'VIRTUAL_BIKE', 'MTB', 'RUN']; + readonly direction = Platform.DIRECTION_TR_INT + readonly planType = [ + {name: 'Plan', value: true}, + {name: 'Folder', value: false} + ] + + trainingTypes = [ + {title: "Ride", value: "BIKE"}, + {title: "Virtual Ride", value: "VIRTUAL_BIKE"}, + {title: "Unknown", value: "UNKNOWN"}, + ] formGroup: FormGroup = this.formBuilder.group({ name: ['My New Library', Validators.required], @@ -53,13 +62,6 @@ export class TrCopyCalendarToLibraryComponent implements OnInit { inProgress = false - trainingTypes = TrainerRoadTrainingTypes.trainingTypes; - - readonly planType = [ - {name: 'Plan', value: true}, - {name: 'Folder', value: false} - ] - constructor( private formBuilder: FormBuilder, private workoutClient: WorkoutClient, diff --git a/ui/src/app/trainer-road/tr-copy-library-to-library/tr-copy-library-to-library.component.ts b/ui/src/app/trainer-road/tr-copy-library-to-library/tr-copy-library-to-library.component.ts index 0b600e74..f01a3725 100644 --- a/ui/src/app/trainer-road/tr-copy-library-to-library/tr-copy-library-to-library.component.ts +++ b/ui/src/app/trainer-road/tr-copy-library-to-library/tr-copy-library-to-library.component.ts @@ -12,7 +12,6 @@ import {MatSnackBarModule} from "@angular/material/snack-bar"; import {MatSelectModule} from "@angular/material/select"; import {MatCheckboxModule} from "@angular/material/checkbox"; import {WorkoutClient} from "infrastructure/client/workout.client"; -import {ConfigurationClient} from "infrastructure/client/configuration.client"; import {NotificationService} from "infrastructure/notification.service"; import {debounceTime, filter, finalize, map, Observable, switchMap, tap} from "rxjs"; import {LibraryClient} from "infrastructure/client/library-client.service"; @@ -61,7 +60,6 @@ export class TrCopyLibraryToLibraryComponent implements OnInit { private formBuilder: FormBuilder, private workoutClient: WorkoutClient, private planClient: LibraryClient, - private configurationClient: ConfigurationClient, private notificationService: NotificationService ) { } diff --git a/ui/src/app/trainer-road/trainer-road-training-types.ts b/ui/src/app/trainer-road/trainer-road-training-types.ts deleted file mode 100644 index 80d703b8..00000000 --- a/ui/src/app/trainer-road/trainer-road-training-types.ts +++ /dev/null @@ -1,7 +0,0 @@ -export class TrainerRoadTrainingTypes { - static trainingTypes = [ - {"title": "Ride", "value": "BIKE"}, - {"title": "Virtual Ride", "value": "VIRTUAL_BIKE"}, - {"title": "Unknown", "value": "UNKNOWN"}, - ] -} diff --git a/ui/src/app/training-peaks/tp-copy-calendar-to-calendar/tp-copy-calendar-to-calendar.component.html b/ui/src/app/training-peaks/tp-copy-calendar-to-calendar/tp-copy-calendar-to-calendar.component.html index b565718d..ab077869 100644 --- a/ui/src/app/training-peaks/tp-copy-calendar-to-calendar/tp-copy-calendar-to-calendar.component.html +++ b/ui/src/app/training-peaks/tp-copy-calendar-to-calendar/tp-copy-calendar-to-calendar.component.html @@ -1,80 +1,5 @@ -
-
- - Direction - - @for (item of directions; track item) { - {{ item.title }} - } - - -
- -
- - Workout Types - - @for (type of trainingTypes; track type) { - {{ type.title }} - } - - -
- -
- - Date Range - - - - - - - -
- -
- - Skip already synced workouts - -
- -
- - - -
- - -
+ + diff --git a/ui/src/app/training-peaks/tp-copy-calendar-to-calendar/tp-copy-calendar-to-calendar.component.scss b/ui/src/app/training-peaks/tp-copy-calendar-to-calendar/tp-copy-calendar-to-calendar.component.scss index 1b1e613c..e69de29b 100644 --- a/ui/src/app/training-peaks/tp-copy-calendar-to-calendar/tp-copy-calendar-to-calendar.component.scss +++ b/ui/src/app/training-peaks/tp-copy-calendar-to-calendar/tp-copy-calendar-to-calendar.component.scss @@ -1,5 +0,0 @@ -.date-range-text { - padding-top: 10px; - padding-bottom: 10px; - padding-left: 10px; -} diff --git a/ui/src/app/training-peaks/tp-copy-calendar-to-calendar/tp-copy-calendar-to-calendar.component.ts b/ui/src/app/training-peaks/tp-copy-calendar-to-calendar/tp-copy-calendar-to-calendar.component.ts index d9852c42..c09afafe 100644 --- a/ui/src/app/training-peaks/tp-copy-calendar-to-calendar/tp-copy-calendar-to-calendar.component.ts +++ b/ui/src/app/training-peaks/tp-copy-calendar-to-calendar/tp-copy-calendar-to-calendar.component.ts @@ -1,22 +1,19 @@ import {Component, OnInit} from '@angular/core'; -import {FormBuilder, FormGroup, FormsModule, ReactiveFormsModule, Validators} from "@angular/forms"; +import {FormsModule, ReactiveFormsModule} from "@angular/forms"; import {MatButtonModule} from "@angular/material/button"; import {MatFormFieldModule} from "@angular/material/form-field"; import {MatInputModule} from "@angular/material/input"; import {MatProgressBarModule} from "@angular/material/progress-bar"; -import {NgIf} from "@angular/common"; import {MatDatepickerModule} from "@angular/material/datepicker"; import {MatNativeDateModule} from "@angular/material/core"; import {MatSnackBarModule} from "@angular/material/snack-bar"; import {MatSelectModule} from "@angular/material/select"; import {MatCheckboxModule} from "@angular/material/checkbox"; -import {WorkoutClient} from "infrastructure/client/workout.client"; -import {NotificationService} from "infrastructure/notification.service"; -import {finalize} from "rxjs"; import {Platform} from "infrastructure/platform"; -import {formatDate} from "utils/date-formatter"; -import {TrainingPeaksTrainingTypes} from "app/training-peaks/training-peaks-training-types"; -import {ConfigurationClient} from "infrastructure/client/configuration.client"; +import {MatListModule} from "@angular/material/list"; +import { + CopyCalendarToCalendarComponent +} from "app/components/copy-calendar-to-calendar/copy-calendar-to-calendar.component"; @Component({ selector: 'tp-copy-calendar-to-calendar', @@ -28,94 +25,38 @@ import {ConfigurationClient} from "infrastructure/client/configuration.client"; MatInputModule, ReactiveFormsModule, MatProgressBarModule, - NgIf, MatDatepickerModule, MatNativeDateModule, MatSnackBarModule, MatSelectModule, MatCheckboxModule, + MatListModule, + CopyCalendarToCalendarComponent, ], templateUrl: './tp-copy-calendar-to-calendar.component.html', styleUrl: './tp-copy-calendar-to-calendar.component.scss' }) export class TpCopyCalendarToCalendarComponent implements OnInit { - readonly trainingTypes = TrainingPeaksTrainingTypes.trainingTypes; - readonly todayDate = new Date() - readonly tomorrowDate = new Date(new Date().getTime() + 24 * 60 * 60 * 1000) + readonly Platform = Platform; readonly directions = [ {title: "Intervals.icu -> TrainingPeaks", value: Platform.DIRECTION_INT_TP}, {title: "TrainingPeaks -> Intervals.icu", value: Platform.DIRECTION_TP_INT}, ] + readonly trainingTypes = [ + {title: "Ride", value: "BIKE"}, + {title: "MTB", value: "MTB"}, + {title: "Virtual Ride", value: "VIRTUAL_BIKE"}, + {title: "Run", value: "RUN"}, + {title: "Swim", value: "SWIM"}, + {title: "Walk", value: "WALK"}, + {title: "Weight Training", value: "WEIGHT"}, + {title: "Any other", value: "UNKNOWN"}, + ] readonly selectedTrainingTypes = ['BIKE', 'VIRTUAL_BIKE', 'MTB', 'RUN']; - formGroup: FormGroup = this.formBuilder.group({ - direction: [this.directions[0].value, Validators.required], - trainingTypes: [this.selectedTrainingTypes, Validators.required], - startDate: [this.todayDate, Validators.required], - endDate: [this.tomorrowDate, Validators.required], - skipSynced: [true, Validators.required], - }); - - inProgress = false - platformInfo: any = null - - constructor( - private formBuilder: FormBuilder, - private workoutClient: WorkoutClient, - private configurationClient: ConfigurationClient, - private notificationService: NotificationService - ) { + constructor() { } ngOnInit(): void { - this.configurationClient.platformInfo(Platform.TRAINING_PEAKS.key).subscribe(value => { - this.platformInfo = value - }) - this.listenDirectionChange() - } - - submit() { - let startDate = formatDate(this.formGroup.controls['startDate'].value) - let endDate = formatDate(this.formGroup.controls['endDate'].value) - this.copyWorkouts(startDate, endDate); - } - - today() { - this.copyWorkoutsForOneDay(formatDate(this.todayDate)); - } - - tomorrow() { - this.copyWorkoutsForOneDay(formatDate(this.tomorrowDate)); - } - - private copyWorkoutsForOneDay(date) { - this.copyWorkouts(date, date) - } - - private copyWorkouts(startDate, endDate) { - this.inProgress = true - let direction = this.formGroup.value.direction - let trainingTypes = this.formGroup.value.trainingTypes - let skipSynced = this.formGroup.value.skipSynced - this.workoutClient.copyCalendarToCalendar(startDate, endDate, trainingTypes, skipSynced, direction).pipe( - finalize(() => this.inProgress = false) - ).subscribe((response) => { - this.notificationService.success( - `Planned: ${response.copied}\n Filtered out: ${response.filteredOut}\n From ${response.startDate} to ${response.endDate}`) - }) - } - - private listenDirectionChange() { - this.formGroup.controls['direction'].valueChanges.subscribe(value => { - if (value === Platform.DIRECTION_INT_TP) { - this.formGroup.controls['skipSynced'].enable() - this.formGroup.controls['skipSynced'].setValue(true) - } else { - this.formGroup.controls['skipSynced'].disable() - this.formGroup.controls['skipSynced'].setValue(false) - } - }) } - - protected readonly Platform = Platform; } diff --git a/ui/src/app/training-peaks/tp-copy-calendar-to-library/tp-copy-calendar-to-library.component.ts b/ui/src/app/training-peaks/tp-copy-calendar-to-library/tp-copy-calendar-to-library.component.ts index d79e9c94..e9d58801 100644 --- a/ui/src/app/training-peaks/tp-copy-calendar-to-library/tp-copy-calendar-to-library.component.ts +++ b/ui/src/app/training-peaks/tp-copy-calendar-to-library/tp-copy-calendar-to-library.component.ts @@ -16,7 +16,6 @@ import {MatSnackBarModule} from "@angular/material/snack-bar"; import {MatSelectModule} from "@angular/material/select"; import {MatCheckboxModule} from "@angular/material/checkbox"; import {Platform} from "infrastructure/platform"; -import {TrainingPeaksTrainingTypes} from "app/training-peaks/training-peaks-training-types"; @Component({ selector: 'tp-copy-calendar-to-library', @@ -40,8 +39,23 @@ import {TrainingPeaksTrainingTypes} from "app/training-peaks/training-peaks-trai styleUrl: './tp-copy-calendar-to-library.component.scss' }) export class TpCopyCalendarToLibraryComponent implements OnInit { - private readonly selectedTrainingTypes = ['BIKE', 'VIRTUAL_BIKE', 'MTB', 'RUN']; - private readonly direction = Platform.DIRECTION_TP_INT + readonly selectedTrainingTypes = ['BIKE', 'VIRTUAL_BIKE', 'MTB', 'RUN']; + readonly direction = Platform.DIRECTION_TP_INT + readonly planType = [ + {name: 'Plan', value: true}, + {name: 'Folder', value: false} + ] + + trainingTypes = [ + {title: "Ride", value: "BIKE"}, + {title: "MTB", value: "MTB"}, + {title: "Virtual Ride", value: "VIRTUAL_BIKE"}, + {title: "Run", value: "RUN"}, + {title: "Swim", value: "SWIM"}, + {title: "Walk", value: "WALK"}, + {title: "Weight Training", value: "WEIGHT"}, + {title: "Any other", value: "UNKNOWN"}, + ] formGroup: FormGroup = this.formBuilder.group({ name: ['My New Library', Validators.required], @@ -50,15 +64,8 @@ export class TpCopyCalendarToLibraryComponent implements OnInit { endDate: [null, Validators.required], isPlan: [true, Validators.required], }); - inProgress = false - trainingTypes = TrainingPeaksTrainingTypes.trainingTypes; - readonly planType = [ - {name: 'Plan', value: true}, - {name: 'Folder', value: false} - ] - constructor( private formBuilder: FormBuilder, private workoutClient: WorkoutClient, diff --git a/ui/src/app/training-peaks/training-peaks-training-types.ts b/ui/src/app/training-peaks/training-peaks-training-types.ts deleted file mode 100644 index 170933c4..00000000 --- a/ui/src/app/training-peaks/training-peaks-training-types.ts +++ /dev/null @@ -1,12 +0,0 @@ -export class TrainingPeaksTrainingTypes { - static trainingTypes = [ - {"title": "Ride", "value": "BIKE"}, - {"title": "MTB", "value": "MTB"}, - {"title": "Virtual Ride", "value": "VIRTUAL_BIKE"}, - {"title": "Run", "value": "RUN"}, - {"title": "Swim", "value": "SWIM"}, - {"title": "Walk", "value": "WALK"}, - {"title": "Weight Training", "value": "WEIGHT"}, - {"title": "Any other", "value": "UNKNOWN"}, - ] -} diff --git a/ui/src/infrastructure/client/configuration.client.ts b/ui/src/infrastructure/client/configuration.client.ts index 8ec6e60f..d6916bb6 100644 --- a/ui/src/infrastructure/client/configuration.client.ts +++ b/ui/src/infrastructure/client/configuration.client.ts @@ -24,6 +24,15 @@ export class ConfigurationClient { .put(`/api/configuration`, configData) } + getAllPlatformInfo(): Observable { + return this.httpClient.get(`/api/configuration/platform`).pipe( + map(response => { + Object.keys(response).forEach(key => response[key] = response[key].infoMap) + return response + }) + ) + } + platformInfo(platform): Observable { return this.httpClient.get(`/api/configuration/${platform}`).pipe( map(response => (response).infoMap) diff --git a/ui/src/infrastructure/client/workout.client.ts b/ui/src/infrastructure/client/workout.client.ts index 6481a8f7..1da51edd 100644 --- a/ui/src/infrastructure/client/workout.client.ts +++ b/ui/src/infrastructure/client/workout.client.ts @@ -33,4 +33,22 @@ export class WorkoutClient { return this.httpClient.get(`/api/workout/find`, {params: {platform, name}}) } + scheduleCopyCalendarToCalendar(startDate, endDate, types, skipSynced, platformDirection): Observable { + return this.httpClient + .post(`/api/workout/copy-calendar-to-calendar/schedule`, { + startDate, + endDate, + types, + skipSynced, + ...platformDirection + }) + } + + getScheduleRequests(): Observable { + return this.httpClient.get(`/api/workout/copy-calendar-to-calendar/schedule`) + } + + deleteScheduleRequest(id: any) { + return this.httpClient.delete(`/api/workout/copy-calendar-to-calendar/schedule/${id}`) + } } diff --git a/ui/src/infrastructure/environment.service.ts b/ui/src/infrastructure/environment.service.ts index f0484deb..45a6bdab 100644 --- a/ui/src/infrastructure/environment.service.ts +++ b/ui/src/infrastructure/environment.service.ts @@ -7,15 +7,17 @@ import {map, Observable} from 'rxjs'; providedIn: 'root' }) export class EnvironmentService { - readonly defaultPort = 8080; readonly electronPort = 44864; constructor(private httpClient: HttpClient) { } getAddress() { - let port = window.electron ? this.electronPort : this.defaultPort - return `http://localhost:${port}` + if (window.electron) { + return `http://localhost:${this.electronPort}` + } else { + return '' + } } getVersion(): Observable { diff --git a/ui/src/infrastructure/platform.ts b/ui/src/infrastructure/platform.ts index 0525a325..40eab2a7 100644 --- a/ui/src/infrastructure/platform.ts +++ b/ui/src/infrastructure/platform.ts @@ -4,6 +4,9 @@ export class Platform { static INTERVALS = {key: 'INTERVALS', title: 'Intervals.icu'} static TRAINING_PEAKS = {key: 'TRAINING_PEAKS', title: 'TrainingPeaks'} static TRAINER_ROAD = {key: 'TRAINER_ROAD', title: 'TrainerRoad'} + static platforms = [ + this.INTERVALS, this.TRAINING_PEAKS, this.TRAINER_ROAD + ] static DIRECTION_TP_INT = { sourcePlatform: this.TRAINING_PEAKS.key, targetPlatform: this.INTERVALS.key @@ -17,4 +20,8 @@ export class Platform { static DIRECTION_TR_TP = { sourcePlatform: this.TRAINER_ROAD.key, targetPlatform: this.TRAINING_PEAKS.key } + + static getTitle(key) { + return this.platforms.find(platform => platform.key === key)?.title + } } diff --git a/ui/src/infrastructure/training-types.ts b/ui/src/infrastructure/training-types.ts new file mode 100644 index 00000000..02d4711e --- /dev/null +++ b/ui/src/infrastructure/training-types.ts @@ -0,0 +1,16 @@ +export class TrainingTypes { + static trainingTypes = [ + {title: "Ride", value: "BIKE"}, + {title: "MTB", value: "MTB"}, + {title: "Virtual Ride", value: "VIRTUAL_BIKE"}, + {title: "Run", value: "RUN"}, + {title: "Swim", value: "SWIM"}, + {title: "Walk", value: "WALK"}, + {title: "Weight Training", value: "WEIGHT"}, + {title: "Any other", value: "UNKNOWN"}, + ] + + static getTitle(value: string) { + return this.trainingTypes.find(type => type.value === value)?.title + } +} diff --git a/ui/src/styles.scss b/ui/src/styles.scss index 8a461aab..d30dbdc0 100644 --- a/ui/src/styles.scss +++ b/ui/src/styles.scss @@ -34,7 +34,7 @@ body { margin: 0; font-family: Roboto, "Helvetica Neue", sans-serif; } } .action-button-section { - margin-bottom: 5px; + margin-top: 10px; } .beta-icon {