Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ public static boolean openFile(final String filePath) {
final File file = new File(filePath);
final Runtime rt = Runtime.getRuntime();

if (!file.exists() || file.isFile()) {
if (!file.exists() || !file.isFile()) {
LOG.warn("Filepath does not seem to exist or does not point to a file: '{}'.", filePath);
return false;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ public List<Mapping> getTableRows(final LocalDate currentReportDate, final List<
String heimatNotes = "";
long heimatTimeSeconds = 0;
boolean isMappedInHeimat = false;
String bookingHint = "";
final Optional<ExternalProjectMapping> optHeimatMapping = mappedProjects.stream()
.filter(mp -> mp.getProject().getId()
== project.getId())
Expand All @@ -118,6 +119,11 @@ public List<Mapping> getTableRows(final LocalDate currentReportDate, final List<
Optional<Mapping> optionalExistingMapping = Optional.empty();
if (optHeimatMapping.isPresent()) {
isMappedInHeimat = true;
bookingHint = heimatTasks.stream()
.filter(ht -> ht.id() == optHeimatMapping.get().getExternalTaskId())
.map(HeimatTask::bookingHint)
.findAny()
.orElseGet(String::new);
optionalExistingMapping = list.stream()
.filter(mapping -> mapping.heimatTaskId == optHeimatMapping.get()
.getExternalTaskId())
Expand Down Expand Up @@ -145,16 +151,20 @@ public List<Mapping> getTableRows(final LocalDate currentReportDate, final List<
pr.appendToWorkNotes(currentWorkNote);
}
final String keeptimeNotes = pr.getNotes();
String canBeSyncedMessage;
StyledMessage canBeSyncedMessage;

if (!isMappedInHeimat) {
canBeSyncedMessage = "Not mapped to Heimat task.\nMap in settings dialog.";
canBeSyncedMessage = StyledMessage.of(
new StyledMessage.TextSegment("Not mapped to Heimat task.\nMap in settings dialog."));
} else if (heimatTasks.stream().noneMatch(ht -> ht.id() == optHeimatMapping.get().getExternalTaskId())) {
canBeSyncedMessage = "Heimat Task is not available (anymore).\nPlease check mappings in settings dialog.";
canBeSyncedMessage = StyledMessage.of(new StyledMessage.TextSegment(
"Heimat Task is not available (anymore).\nPlease check mappings in settings dialog."));
isMappedInHeimat = false;
} else {
final ExternalProjectMapping externalProjectMapping = optHeimatMapping.get();
canBeSyncedMessage = "Sync to " + externalProjectMapping.getExternalTaskName() + "\n("
+ externalProjectMapping.getExternalProjectName() + ")";
canBeSyncedMessage = StyledMessage.of(new StyledMessage.TextSegment("Sync to "),
new StyledMessage.TextSegment(externalProjectMapping.getExternalTaskName(), true),
new StyledMessage.TextSegment("\n(" + externalProjectMapping.getExternalProjectName() + ")"));
}

if (optionalExistingMapping.isPresent()) {
Expand All @@ -166,18 +176,18 @@ public List<Mapping> getTableRows(final LocalDate currentReportDate, final List<
final boolean shouldBeSynced =
isMappedInHeimat && differenceGreaterOrEqual15Minutes(heimatSeconds, keepTimeSeconds);
final Mapping mapping = new Mapping(isMappedInHeimat ? optHeimatMapping.get().getExternalTaskId() : -1,
isMappedInHeimat, shouldBeSynced, canBeSyncedMessage, existingMapping.existingTimes(), projects,
existingMapping.heimatNotes(), existingMapping.keeptimeNotes() + ". " + keeptimeNotes, heimatSeconds,
keepTimeSeconds);
isMappedInHeimat, shouldBeSynced, canBeSyncedMessage, bookingHint, existingMapping.existingTimes(),
projects, existingMapping.heimatNotes(), existingMapping.keeptimeNotes() + ". " + keeptimeNotes,
heimatSeconds, keepTimeSeconds);
list.remove(existingMapping);
list.add(mapping);
} else {
final boolean shouldBeSynced =
isMappedInHeimat && differenceGreaterOrEqual15Minutes(heimatTimeSeconds, projectWorkSeconds);
final List<Project> projects = Collections.singletonList(project);
final Mapping mapping = new Mapping(isMappedInHeimat ? optHeimatMapping.get().getExternalTaskId() : -1,
isMappedInHeimat, shouldBeSynced, canBeSyncedMessage, optionalAlreadyBookedTimes, projects,
heimatNotes, keeptimeNotes, heimatTimeSeconds, projectWorkSeconds);
isMappedInHeimat, shouldBeSynced, canBeSyncedMessage, bookingHint, optionalAlreadyBookedTimes,
projects, heimatNotes, keeptimeNotes, heimatTimeSeconds, projectWorkSeconds);
list.add(mapping);
}
}
Expand All @@ -193,15 +203,17 @@ public List<Mapping> getTableRows(final LocalDate currentReportDate, final List<
long heimatTimeSeconds = times.stream()
.reduce(0L, (subtotal, element) -> subtotal + element.durationInMinutes() * 60L,
Long::sum);
final Optional<HeimatTask> optionalHeimatTask = heimatTasks.stream()
.filter(t -> t.id() == id)
.findAny();

final Optional<HeimatTask> optionalHeimatTask = heimatTasks.stream().filter(t -> t.id() == id).findAny();
String taskName = "Cannot resolve Heimat Task Id: " + id + " to name\nPlease check in Heimat";
if (optionalHeimatTask.isPresent()) {
final HeimatTask heimatTask = optionalHeimatTask.get();
taskName = heimatTask.name() + "\n" + heimatTask.taskHolderName();
}
final Mapping mapping = new Mapping(id, true, false, "Not mapped in KeepTime\n\n" + taskName, times,

final Mapping mapping = new Mapping(id, true, false,
StyledMessage.of(new StyledMessage.TextSegment("Not mapped in KeepTime\n\n" + taskName)), "", times,

new ArrayList<>(0), heimatNotes, "", heimatTimeSeconds, 0);
list.add(mapping);
});
Expand All @@ -224,14 +236,17 @@ public List<Mapping> getTableRows(final LocalDate currentReportDate, final List<
String heimatNotes = addHeimatNotes(times);
long heimatTimeSeconds = addHeimatTimes(times);

final Mapping mapping2 = new Mapping(id, true, false,
"Present in HEIMAT but not KeepTime\n\nSync to " + externalProjectMapping.getExternalTaskName() + "\n("
+ externalProjectMapping.getExternalProjectName() + ")", times, mappedProjects.stream()
.filter(
mp -> mp.getExternalTaskId()
== id)
.map(ExternalProjectMapping::getProject)
.toList(),
StyledMessage syncMessage = StyledMessage.of(
new StyledMessage.TextSegment("Present in HEIMAT but not KeepTime\n\nSync to "),
new StyledMessage.TextSegment(externalProjectMapping.getExternalTaskName(), true),
new StyledMessage.TextSegment("\n(" + externalProjectMapping.getExternalProjectName() + ")"));

final Mapping mapping2 = new Mapping(id, true, false, syncMessage, "", times, mappedProjects.stream()
.filter(
mp -> mp.getExternalTaskId()
== id)
.map(ExternalProjectMapping::getProject)
.toList(),
heimatNotes, "", heimatTimeSeconds, 0);
list.add(mapping2);
});
Expand Down Expand Up @@ -424,8 +439,8 @@ public ExistingAndInvalidMappings getExistingProjectMappings(List<HeimatTask> ex

public record UserMapping(Mapping mapping, boolean shouldSync, String userNotes, int userMinutes) {}

public record Mapping(long heimatTaskId, boolean canBeSynced, boolean shouldBeSynced, String syncMessage,
List<HeimatTime> existingTimes, List<Project> projects, String heimatNotes,
public record Mapping(long heimatTaskId, boolean canBeSynced, boolean shouldBeSynced, StyledMessage syncMessage,
String bookingHint, List<HeimatTime> existingTimes, List<Project> projects, String heimatNotes,
String keeptimeNotes, long heimatSeconds, long keeptimeSeconds) {}

public record HeimatErrors(UserMapping mapping, String errorMessage) {}
Expand Down
70 changes: 70 additions & 0 deletions src/main/java/de/doubleslash/keeptime/model/StyledMessage.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
// Copyright 2025 doubleSlash Net Business GmbH
//
// This file is part of KeepTime.
// KeepTime is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.

package de.doubleslash.keeptime.model;

import java.util.ArrayList;
import java.util.List;

/**
* Represents a styled text message composed of multiple text segments. This class provides a UI-agnostic way to
* represent formatted text, allowing separation of business logic from UI components.
*/
public class StyledMessage {

/**
* Represents a single text segment with optional styling.
*
* @param text
* The text content
* @param bold
* Whether the text should be displayed in bold
*/
public record TextSegment(String text, boolean bold) {
public TextSegment(String text) {
this(text, false);
}
}

private final List<TextSegment> segments;

public StyledMessage(List<TextSegment> segments) {
this.segments = new ArrayList<>(segments);
}

/**
* Creates a StyledMessage from a variable number of text segments.
*
* @param segments
* The text segments to include in the message
* @return A new StyledMessage containing the provided segments
*/
public static StyledMessage of(TextSegment... segments) {
return new StyledMessage(List.of(segments));
}

public List<TextSegment> getSegments() {
return new ArrayList<>(segments);
}

/**
* Returns the message as plain text without styling.
*/
public String toPlainText() {
return segments.stream().map(TextSegment::text).reduce("", String::concat);
}
}

Loading
Loading