Skip to content

Commit 87cf1c3

Browse files
committed
PTBAS-741: Fix issues
* improve test * improve expiration label * refactor code in ExternalProjectsMapController * fix work stacking when it is not needed
1 parent 6fbccae commit 87cf1c3

File tree

4 files changed

+91
-70
lines changed

4 files changed

+91
-70
lines changed

src/main/java/de/doubleslash/keeptime/controller/HeimatController.java

Lines changed: 15 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -171,28 +171,15 @@ public List<Mapping> getTableRows(final LocalDate currentReportDate, final List<
171171
if (optionalExistingMapping.isPresent()) {
172172
final Mapping existingMapping = optionalExistingMapping.get();
173173

174-
// Ensure we merge projects robustly: include any projects mapped to the same Heimat task
174+
// Only include projects that were actually worked on at this date
175175
final ArrayList<Project> projects = new ArrayList<>(existingMapping.projects());
176-
if (optHeimatMapping.isPresent()) {
177-
final long mappedTaskId = optHeimatMapping.get().getExternalTaskId();
178-
final List<Project> allMappedForTask = mappedProjects.stream()
179-
.filter(
180-
mp -> mp.getExternalTaskId() == mappedTaskId)
181-
.map(ExternalProjectMapping::getProject)
182-
.toList();
183-
for (Project p : allMappedForTask) {
184-
boolean alreadyContains = projects.stream().anyMatch(pp -> pp.getId() == p.getId());
185-
if (!alreadyContains) {
186-
projects.add(p);
187-
}
188-
}
189-
} else {
190-
// fallback: ensure current project is present
191-
boolean alreadyContains = projects.stream().anyMatch(p -> p.getId() == project.getId());
192-
if (!alreadyContains) {
193-
projects.add(project);
194-
}
176+
// Ensure current project is present (it's already in workedProjectsSet, so it was worked on)
177+
boolean alreadyContains = projects.stream().anyMatch(p -> p.getId() == project.getId());
178+
if (!alreadyContains) {
179+
projects.add(project);
195180
}
181+
// Filter to only keep projects that were actually worked on
182+
projects.removeIf(p -> !workedProjectsSet.contains(p));
196183

197184
final long keepTimeSeconds = existingMapping.keeptimeSeconds() + projectWorkSeconds;
198185
final long heimatSeconds = existingMapping.heimatSeconds();
@@ -214,15 +201,14 @@ public List<Mapping> getTableRows(final LocalDate currentReportDate, final List<
214201
list.remove(existingMapping);
215202
list.add(mapping);
216203
} else {
217-
// FIX: when creating a new mapping row for a project that is mapped to a Heimat task,
218-
// include ALL Projects that are mapped to the same Heimat task so that projects without
219-
// KeepTime entries (no worked time) are also shown in the same row (case 4).
204+
// Only include projects that were actually worked on at this date
220205
final List<Project> projects;
221206
if (optHeimatMapping.isPresent()) {
222207
final long mappedTaskId = optHeimatMapping.get().getExternalTaskId();
223208
projects = mappedProjects.stream()
224209
.filter(mp -> mp.getExternalTaskId() == mappedTaskId)
225210
.map(ExternalProjectMapping::getProject)
211+
.filter(workedProjectsSet::contains)
226212
.collect(Collectors.toList());
227213
} else {
228214
projects = Collections.singletonList(project);
@@ -256,9 +242,12 @@ public List<Mapping> getTableRows(final LocalDate currentReportDate, final List<
256242
taskName = heimatTask.name() + "\n" + heimatTask.taskHolderName();
257243
}
258244

259-
final Mapping mapping = new Mapping(id, true, false,
260-
StyledMessage.of(new StyledMessage.TextSegment("Not mapped in KeepTime\n\n" + taskName)), "", times,
245+
// Build sync message with task name in bold
246+
StyledMessage syncMessage = StyledMessage.of(
247+
new StyledMessage.TextSegment("Not mapped in KeepTime\n\n"),
248+
new StyledMessage.TextSegment(taskName, true));
261249

250+
final Mapping mapping = new Mapping(id, true, false, syncMessage, "", times,
262251
new ArrayList<>(0), heimatNotes, "", heimatTimeSeconds, 0);
263252
list.add(mapping);
264253
});
@@ -281,23 +270,8 @@ public List<Mapping> getTableRows(final LocalDate currentReportDate, final List<
281270
long heimatTimeSeconds = addHeimatTimes(times);
282271

283272
final Optional<Mapping> existingMappingInList = list.stream().filter(m -> m.heimatTaskId == id).findAny();
273+
// If there's already a mapping from worked-on projects, don't add non-worked projects to it
284274
if (existingMappingInList.isPresent()) {
285-
Mapping existing = existingMappingInList.get();
286-
// only add if not already present
287-
boolean alreadyContains = existing.projects()
288-
.stream()
289-
.anyMatch(
290-
p -> p.getId() == externalProjectMapping.getProject().getId());
291-
if (!alreadyContains) {
292-
ArrayList<Project> newProjects = new ArrayList<>(existing.projects());
293-
newProjects.add(externalProjectMapping.getProject());
294-
Mapping updated = new Mapping(existing.heimatTaskId, existing.canBeSynced, existing.shouldBeSynced,
295-
existing.syncMessage, existing.bookingHint, existing.existingTimes, newProjects,
296-
existing.heimatNotes, existing.keeptimeNotes, existing.heimatSeconds, existing.keeptimeSeconds);
297-
list.remove(existing);
298-
list.add(updated);
299-
}
300-
// skip creating a separate entry
301275
continue;
302276
}
303277

src/main/java/de/doubleslash/keeptime/view/ExternalProjectsMapController.java

Lines changed: 19 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@
2525
import de.doubleslash.keeptime.rest.integration.heimat.model.ExistingAndInvalidMappings;
2626
import de.doubleslash.keeptime.rest.integration.heimat.model.HeimatTask;
2727
import de.doubleslash.keeptime.viewpopup.SearchCombobox;
28-
import javafx.application.Platform;
2928
import javafx.beans.property.SimpleBooleanProperty;
3029
import javafx.beans.property.SimpleObjectProperty;
3130
import javafx.beans.property.SimpleStringProperty;
@@ -80,8 +79,25 @@ public ExternalProjectsMapController(final Model model, Controller controller, H
8079
this.heimatController = heimatController;
8180
}
8281

82+
private ExistingAndInvalidMappings existingAndInvalidMappings;
83+
private ObservableList<HeimatController.ProjectMapping> newProjectMappings;
84+
8385
public void setStage(final Stage thisStage) {
8486
this.thisStage = thisStage;
87+
// Show invalid mappings dialog when stage is shown
88+
thisStage.setOnShown(e -> {
89+
if (existingAndInvalidMappings != null && newProjectMappings != null) {
90+
List<String> warnings = existingAndInvalidMappings.invalidMappingsAsString();
91+
if (!warnings.isEmpty()) {
92+
if (showInvalidMappingsDialog(warnings)) {
93+
newProjectMappings.stream()
94+
.filter(HeimatController.ProjectMapping::isPendingRemoval)
95+
.forEach(pm -> pm.setHeimatTask(null));
96+
mappingTableView.refresh();
97+
}
98+
}
99+
}
100+
});
85101
}
86102

87103
@FXML
@@ -92,26 +108,14 @@ private void initialize() {
92108

93109
final List<HeimatTask> externalProjects = heimatController.getAllKnownHeimatTasks(tasksForDateDatePicker.getValue());
94110

95-
final ExistingAndInvalidMappings existingAndInvalidMappings = heimatController.getExistingProjectMappings(
111+
existingAndInvalidMappings = heimatController.getExistingProjectMappings(
96112
externalProjects);
97113

98114

99115
final List<HeimatController.ProjectMapping> previousProjectMappings = existingAndInvalidMappings.validMappings();
100-
final ObservableList<HeimatController.ProjectMapping> newProjectMappings = FXCollections.observableArrayList(
116+
newProjectMappings = FXCollections.observableArrayList(
101117
previousProjectMappings);
102118

103-
Platform.runLater(() -> {
104-
List<String> warnings = existingAndInvalidMappings.invalidMappingsAsString();
105-
if (!warnings.isEmpty()) {
106-
if (showInvalidMappingsDialog(warnings)) {
107-
newProjectMappings.stream()
108-
.filter(HeimatController.ProjectMapping::isPendingRemoval)
109-
.forEach(pm -> pm.setHeimatTask(null));
110-
mappingTableView.refresh();
111-
}
112-
}
113-
});
114-
115119
final FilteredList<HeimatController.ProjectMapping> value = new FilteredList<>(newProjectMappings,
116120
pm -> pm.getProject().isWork());
117121
mappingTableView.setItems(value);

src/main/java/de/doubleslash/keeptime/view/SettingsController.java

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -415,21 +415,25 @@ private void initializeHeimat() {
415415
heimatPatTextField.textProperty().addListener((observable, oldValue, newValue)->{
416416
try{
417417
final JwtDecoder.JWTTokenAttributes jwt = JwtDecoder.parse(newValue);
418-
if (!JwtDecoder.isExpired(jwt, LocalDateTime.now())) {
419-
heimatExpiresLabel.setText("Expired:");
420-
heimatExpiresLabel.setTextFill(Color.RED);
418+
final boolean isExpired = !JwtDecoder.isExpired(jwt, LocalDateTime.now());
419+
final String expirationDate = jwt.expiration().toString();
420+
421+
// Keep label text consistent, show expired status in the value
422+
heimatExpiresLabel.setText("Expires:");
423+
heimatExpiresLabel.setTextFill(Color.BLACK);
424+
425+
if (isExpired) {
426+
expirationDateLabel.setText(expirationDate + " (Expired)");
421427
expirationDateLabel.setTextFill(Color.RED);
422428
} else {
423-
heimatExpiresLabel.setText("Expires:");
424-
heimatExpiresLabel.setTextFill(Color.BLACK);
429+
expirationDateLabel.setText(expirationDate);
425430
expirationDateLabel.setTextFill(Color.BLACK);
426431
}
427432

428-
expirationDateLabel.setText(jwt.expiration().toString());
429-
430433
} catch(Exception e){
431-
heimatExpiresLabel.setText("");
434+
heimatExpiresLabel.setText("Expires:");
432435
expirationDateLabel.setText("Does not seem to be valid");
436+
expirationDateLabel.setTextFill(Color.RED);
433437
}
434438
});
435439
heimatValidateConnectionLabel.setText("Not validated.");

src/test/java/de/doubleslash/keeptime/controller/HeimatControllerTest.java

Lines changed: 45 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@
2626
import de.doubleslash.keeptime.rest.integration.heimat.model.HeimatTask;
2727
import de.doubleslash.keeptime.rest.integration.heimat.model.HeimatTime;
2828
import javafx.scene.paint.Color;
29-
import javafx.scene.text.Text;
3029
import org.hamcrest.Matchers;
3130
import org.junit.jupiter.api.BeforeEach;
3231
import org.junit.jupiter.api.Test;
@@ -39,7 +38,6 @@
3938
import java.util.ArrayList;
4039
import java.util.Arrays;
4140
import java.util.List;
42-
import java.util.stream.Collectors;
4341

4442
import static org.hamcrest.MatcherAssert.assertThat;
4543
import static org.junit.jupiter.api.Assertions.*;
@@ -520,7 +518,7 @@ void shouldReturnErrorsWhenErrorsOccurred() {
520518
// shouldOnlyUpdateHeimatWhenSomethingHasChanged (not needed - user should decide)
521519

522520
@Test
523-
void shouldNotCreateDuplicateHeimatEntryWhenMultipleProjectsMappedAndSomeHaveWork() {
521+
void shouldOnlyShowWorkedOnProjectsWhenMultipleProjectsMappedAndSomeHaveWork() {
524522
// ARRANGE
525523
// project 1 has work, project 2 does not
526524
final Work work1 = new Work(now.minusMinutes(10), now, workProject1, "Notes 1");
@@ -542,10 +540,11 @@ void shouldNotCreateDuplicateHeimatEntryWhenMultipleProjectsMappedAndSomeHaveWor
542540
assertThat(tableRows.size(), Matchers.is(1));
543541
final HeimatController.Mapping mapping = tableRows.get(0);
544542

545-
// The mapping should combine both projects in .projects()
546-
assertThat(mapping.projects(), Matchers.containsInAnyOrder(workProject1, workProject2));
543+
// Only project1 should be in the list since project2 was not worked on
544+
assertThat(mapping.projects(), Matchers.containsInAnyOrder(workProject1));
545+
assertThat(mapping.projects().size(), Matchers.is(1));
547546

548-
// The mapping should show KeepTime time for workProject1, and 0 for workProject2 (which is included in .projects() but has no time)
547+
// The mapping should show KeepTime time for workProject1
549548
assertThat(mapping.keeptimeSeconds(), Matchers.is(10 * 60L));
550549
assertThat(mapping.keeptimeNotes(), Matchers.is("Notes 1"));
551550

@@ -556,4 +555,44 @@ void shouldNotCreateDuplicateHeimatEntryWhenMultipleProjectsMappedAndSomeHaveWor
556555
assertThat(mapping.syncMessage().toPlainText(), Matchers.not(Matchers.containsString("Present in HEIMAT but not KeepTime")));
557556
assertThat(mapping.syncMessage().toPlainText(), Matchers.containsString(project1To1Mapping.getExternalTaskName()));
558557
}
558+
559+
@Test
560+
void shouldNotCreateDuplicateHeimatEntryWhenMultipleProjectsMappedAndBothHaveWork() {
561+
// ARRANGE
562+
// Both projects have work and are mapped to the same Heimat task
563+
final Work work1 = new Work(now.minusMinutes(10), now, workProject1, "Notes 1");
564+
workItems.add(work1);
565+
final Work work2 = new Work(now.minusMinutes(5), now, workProject2, "Notes 2");
566+
workItems.add(work2);
567+
568+
externalMappings.add(project1To1Mapping);
569+
externalMappings.add(project2To1Mapping);
570+
571+
// The mapped Heimat task has already been booked in Heimat
572+
final HeimatTime existingTime = new HeimatTime(project1To1Mapping.getExternalTaskId(), now.toLocalDate(), null,
573+
null, 15, "Heimat note", 99);
574+
when(mockedHeimatAPI.getMyTimes(now.toLocalDate())).thenReturn(Arrays.asList(existingTime));
575+
576+
// ACT
577+
final List<HeimatController.Mapping> tableRows = heimatController.getTableRows(now.toLocalDate(), workItems);
578+
579+
// ASSERT
580+
// There should be exactly one row for this Heimat task (no duplicates)
581+
assertThat(tableRows.size(), Matchers.is(1));
582+
final HeimatController.Mapping mapping = tableRows.get(0);
583+
584+
// Both projects should be in the list since both were worked on
585+
assertThat(mapping.projects(), Matchers.containsInAnyOrder(workProject1, workProject2));
586+
587+
// The mapping should combine KeepTime time from both projects
588+
assertThat(mapping.keeptimeSeconds(), Matchers.is((10 + 5) * 60L));
589+
assertThat(mapping.keeptimeNotes(), Matchers.is("Notes 1. Notes 2"));
590+
591+
// There should be Heimat time and notes as well
592+
assertThat(mapping.heimatNotes(), Matchers.is("Heimat note"));
593+
assertThat(mapping.heimatSeconds(), Matchers.is(15 * 60L));
594+
595+
assertThat(mapping.syncMessage().toPlainText(), Matchers.not(Matchers.containsString("Present in HEIMAT but not KeepTime")));
596+
assertThat(mapping.syncMessage().toPlainText(), Matchers.containsString(project1To1Mapping.getExternalTaskName()));
597+
}
559598
}

0 commit comments

Comments
 (0)