Skip to content

Commit 3461ce2

Browse files
committed
#178: show current heimat + keeptime times. add copy button. add hyperlink. make rows non interactable if cannot be synced
1 parent bf62fa9 commit 3461ce2

File tree

6 files changed

+144
-83
lines changed

6 files changed

+144
-83
lines changed

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

Lines changed: 38 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
import org.springframework.stereotype.Service;
1717

1818
import java.time.LocalDate;
19+
import java.time.format.DateTimeFormatter;
1920
import java.util.*;
2021
import java.util.stream.Collectors;
2122

@@ -24,20 +25,24 @@ public class HeimatController {
2425
private static final Logger LOG = LoggerFactory.getLogger(HeimatController.class);
2526

2627
private final Controller controller;
27-
28+
private final HeimatSettings heimatSettings;
2829
private final ExternalProjectsMappingsRepository externalProjectsMappingsRepository;
2930
private final HeimatAPI heimatAPI;
3031

3132
@Autowired
3233
public HeimatController(HeimatSettings heimatSettings,
3334
ExternalProjectsMappingsRepository externalProjectsMappingsRepository, final Controller controller) {
35+
this.heimatSettings = heimatSettings;
3436
this.controller = controller;
3537
this.externalProjectsMappingsRepository = externalProjectsMappingsRepository;
38+
3639
heimatAPI = new HeimatAPI(heimatSettings.getHeimatUrl(), heimatSettings.getHeimatPat());
3740
}
3841

39-
public HeimatController(HeimatAPI heimatAPI, ExternalProjectsMappingsRepository externalProjectsMappingsRepository,
40-
final Controller controller) {
42+
// for testing only
43+
HeimatController(HeimatSettings heimatSettings, HeimatAPI heimatAPI,
44+
ExternalProjectsMappingsRepository externalProjectsMappingsRepository, final Controller controller) {
45+
this.heimatSettings = heimatSettings;
4146
this.controller = controller;
4247
this.externalProjectsMappingsRepository = externalProjectsMappingsRepository;
4348
this.heimatAPI = heimatAPI;
@@ -62,22 +67,22 @@ public List<Mapping> getTableRows(final LocalDate currentReportDate, final List<
6267
long heimatTimeSeconds = 0;
6368
boolean isMappedInHeimat = false;
6469
final Optional<ExternalProjectMapping> optHeimatMapping = mappedProjects.stream()
65-
.filter(mp -> mp.getProject().getId()
66-
== project.getId())
67-
.findAny();
70+
.filter(mp -> mp.getProject().getId()
71+
== project.getId())
72+
.findAny();
6873
List<HeimatTime> optionalAlreadyBookedTimes = new ArrayList<>();
6974
Optional<Mapping> optionalExistingMapping = Optional.empty();
7075
if (optHeimatMapping.isPresent()) {
7176
isMappedInHeimat = true;
7277
optionalExistingMapping = list.stream()
7378
.filter(mapping -> mapping.heimatTaskId == optHeimatMapping.get()
74-
.getExternalTaskId())
79+
.getExternalTaskId())
7580
.findAny();
7681
optionalAlreadyBookedTimes = heimatTimes.stream()
7782
.filter(heimatTime -> optHeimatMapping.stream()
78-
.anyMatch(
79-
hm -> heimatTime.taskId()
80-
== hm.getExternalTaskId()))
83+
.anyMatch(
84+
hm -> heimatTime.taskId()
85+
== hm.getExternalTaskId()))
8186
.toList();
8287
if (!optionalAlreadyBookedTimes.isEmpty()) {
8388
heimatNotes = optionalAlreadyBookedTimes.stream()
@@ -102,13 +107,13 @@ public List<Mapping> getTableRows(final LocalDate currentReportDate, final List<
102107
final String keeptimeNotes = pr.getNotes();
103108
String canBeSynced;
104109
if (!isMappedInHeimat) {
105-
canBeSynced = "Not mapped in Heimat";
110+
canBeSynced = "Not mapped to Heimat task.\nMap in settings dialog.";
106111
} else if (heimatTasks.stream().noneMatch(ht -> ht.id() == optHeimatMapping.get().getExternalTaskId())) {
107-
canBeSynced = "Heimat Task is no longer available.";
112+
canBeSynced = "Heimat Task is not available (anymore).\nPlease check mappings in settings dialog.";
108113
isMappedInHeimat = false;
109114
} else {
110115
final ExternalProjectMapping externalProjectMapping = optHeimatMapping.get();
111-
canBeSynced = "Sync to " + externalProjectMapping.getExternalTaskName() + "("
116+
canBeSynced = "Sync to " + externalProjectMapping.getExternalTaskName() + "\n("
112117
+ externalProjectMapping.getExternalProjectName() + ")";
113118
}
114119

@@ -118,10 +123,8 @@ public List<Mapping> getTableRows(final LocalDate currentReportDate, final List<
118123
projects.add(project);
119124
final Mapping mapping = new Mapping(isMappedInHeimat ? optHeimatMapping.get().getExternalTaskId() : -1,
120125
isMappedInHeimat, canBeSynced, existingMapping.existingTimes(), projects,
121-
existingMapping.heimatNotes(),
122-
existingMapping.keeptimeNotes() + ". " + keeptimeNotes,
123-
existingMapping.heimatSeconds(),
124-
existingMapping.keeptimeSeconds() + projectWorkSeconds);
126+
existingMapping.heimatNotes(), existingMapping.keeptimeNotes() + ". " + keeptimeNotes,
127+
existingMapping.heimatSeconds(), existingMapping.keeptimeSeconds() + projectWorkSeconds);
125128
list.remove(existingMapping);
126129
list.add(mapping);
127130
} else {
@@ -133,18 +136,18 @@ public List<Mapping> getTableRows(final LocalDate currentReportDate, final List<
133136
}
134137
}
135138
final List<Long> mappedIds = mappedProjects.stream().map(ExternalProjectMapping::getExternalTaskId).toList();
136-
final Map<Long, List<HeimatTime>> notMappedExistingTimes = heimatTimes.stream().filter(ht -> !mappedIds.contains(ht.taskId())).collect(
137-
Collectors.groupingBy(HeimatTime::taskId));
138-
notMappedExistingTimes.forEach((id, times)->{
139-
String heimatNotes = times.stream()
140-
.map(HeimatTime::note)
141-
.collect(Collectors.joining(". "));
139+
final Map<Long, List<HeimatTime>> notMappedExistingTimes = heimatTimes.stream()
140+
.filter(ht -> !mappedIds.contains(
141+
ht.taskId()))
142+
.collect(Collectors.groupingBy(
143+
HeimatTime::taskId));
144+
notMappedExistingTimes.forEach((id, times) -> {
145+
String heimatNotes = times.stream().map(HeimatTime::note).collect(Collectors.joining(". "));
142146
long heimatTimeSeconds = times.stream()
143-
.reduce(0L, (subtotal, element) -> subtotal
144-
+ element.durationInMinutes() * 60L, Long::sum);
145-
final Mapping mapping = new Mapping(id,
146-
false, "Not mapped in KeepTime", times, new ArrayList<>(0), heimatNotes, "",
147-
heimatTimeSeconds, 0);
147+
.reduce(0L, (subtotal, element) -> subtotal + element.durationInMinutes() * 60L,
148+
Long::sum);
149+
final Mapping mapping = new Mapping(id, false, "Not mapped in KeepTime", times, new ArrayList<>(0),
150+
heimatNotes, "", heimatTimeSeconds, 0);
148151
list.add(mapping);
149152
});
150153
return list;
@@ -160,7 +163,7 @@ public List<HeimatErrors> saveDay(final List<Asdf> items, LocalDate date) {
160163
}
161164
final int durationInMinutes = item.userMinutes;
162165
if (durationInMinutes <= 0 || durationInMinutes % 15 != 0) {
163-
errors.add(new HeimatErrors(item, "Duration '"+durationInMinutes+"' is not valid for project"));
166+
errors.add(new HeimatErrors(item, "Duration '" + durationInMinutes + "' is not valid for project"));
164167
return;
165168
}
166169

@@ -183,6 +186,11 @@ public List<HeimatErrors> saveDay(final List<Asdf> items, LocalDate date) {
183186
return errors;
184187
}
185188

189+
public String getUrlForDay(final LocalDate currentReportDate) {
190+
return heimatSettings.getHeimatUrl() + "/core/heimat/time/main/day/" + currentReportDate.format(
191+
DateTimeFormatter.ofPattern("yyyy/M/d"));
192+
}
193+
186194
public static class Asdf {
187195
private final Mapping mapping;
188196
private boolean shouldSync;
@@ -229,7 +237,5 @@ public record Mapping(long heimatTaskId, boolean canBeSynced, String syncMessage
229237
List<Project> projects, String heimatNotes, String keeptimeNotes, long heimatSeconds,
230238
long keeptimeSeconds) {}
231239

232-
public record HeimatErrors (
233-
Asdf mapping, String errorMessage
234-
){}
240+
public record HeimatErrors(Asdf mapping, String errorMessage) {}
235241
}

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

Lines changed: 68 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package de.doubleslash.keeptime.view;
22

3+
import de.doubleslash.keeptime.common.BrowserHelper;
4+
import de.doubleslash.keeptime.common.DateFormatter;
35
import de.doubleslash.keeptime.common.Resources;
46
import de.doubleslash.keeptime.common.SvgNodeProvider;
57
import de.doubleslash.keeptime.controller.HeimatController;
@@ -34,13 +36,14 @@
3436

3537
import java.time.LocalDate;
3638
import java.time.LocalTime;
37-
import java.time.format.DateTimeFormatter;
3839
import java.time.format.DateTimeParseException;
3940
import java.time.format.FormatStyle;
4041
import java.util.Collections;
4142
import java.util.List;
4243
import java.util.function.Consumer;
4344

45+
import static de.doubleslash.keeptime.view.ReportController.copyToClipboard;
46+
4447
@Component
4548
public class ExternalProjectsSyncController {
4649

@@ -60,6 +63,10 @@ public class ExternalProjectsSyncController {
6063

6164
@FXML
6265
private Label sumTimeLabel;
66+
@FXML
67+
private Label keepTimeTimeLabel;
68+
@FXML
69+
private Label heimatTimeLabel;
6370

6471
@FXML
6572
private Hyperlink externalSystemLink;
@@ -73,23 +80,26 @@ public class ExternalProjectsSyncController {
7380
@FXML
7481
private Label loadingMessage;
7582

76-
private final SVGPath loadingSpinner = SvgNodeProvider.getSvgNodeWithScale(Resources.RESOURCE.SVG_SPINNER_SOLID, 0.1, 0.1);
77-
private final SVGPath loadingSuccess = SvgNodeProvider.getSvgNodeWithScale(Resources.RESOURCE.SVG_THUMBS_UP_SOLID, 0.1, 0.1);
78-
private final SVGPath loadingFailure = SvgNodeProvider.getSvgNodeWithScale(Resources.RESOURCE.SVG_XMARK_SOLID, 0.1, 0.1);
83+
private final SVGPath loadingSpinner = SvgNodeProvider.getSvgNodeWithScale(Resources.RESOURCE.SVG_SPINNER_SOLID, 0.1,
84+
0.1);
85+
private final SVGPath loadingSuccess = SvgNodeProvider.getSvgNodeWithScale(Resources.RESOURCE.SVG_THUMBS_UP_SOLID,
86+
0.1, 0.1);
87+
private final SVGPath loadingFailure = SvgNodeProvider.getSvgNodeWithScale(Resources.RESOURCE.SVG_XMARK_SOLID, 0.1,
88+
0.1);
7989

8090
private final LocalTimeStringConverter localTimeStringConverter = new LocalTimeStringConverter(FormatStyle.MEDIUM);
8191
private ObservableList<TableRow> items;
8292

8393
private LocalDate currentReportDate;
8494
private Stage thisStage;
85-
private final HeimatController heimatController;
95+
private final HeimatController heimatController;
8696

8797
public ExternalProjectsSyncController(final HeimatController heimatController) {
8898
this.heimatController = heimatController;
8999
}
90100

91101
public void initForDate(LocalDate currentReportDate, List<Work> currentWorkItems) {
92-
dayOfSyncLabel.setText(currentReportDate.format(DateTimeFormatter.BASIC_ISO_DATE));
102+
dayOfSyncLabel.setText(DateFormatter.toDayDateString(currentReportDate));
93103
this.currentReportDate = currentReportDate;
94104

95105
// TODO add a spinner while loading?
@@ -99,7 +109,7 @@ public void initForDate(LocalDate currentReportDate, List<Work> currentWorkItems
99109
items = FXCollections.observableArrayList(tableRows.stream().map(mapping -> {
100110
String userNotes = mapping.keeptimeNotes();
101111
long userSeconds = mapping.keeptimeSeconds();
102-
// use info from heimat
112+
// use info from heimat when already present
103113
if (mapping.heimatSeconds() != 0L) {
104114
userNotes = mapping.heimatNotes();
105115
userSeconds = mapping.heimatSeconds();
@@ -113,13 +123,17 @@ public void initForDate(LocalDate currentReportDate, List<Work> currentWorkItems
113123
item -> new javafx.beans.Observable[] { item.userTimeSeconds, item.shouldSyncCheckBox });
114124
items2.addAll(items);
115125
StringBinding totalSum = Bindings.createStringBinding(() -> localTimeStringConverter.toString(
116-
LocalTime.ofSecondOfDay(items.stream()
117-
.filter(item -> item.shouldSyncCheckBox.get())
118-
.mapToLong(item -> item.userTimeSeconds.getValue())
119-
.sum())), items2);
120-
sumTimeLabel.textProperty().bind(Bindings.concat("Total Sum: ", totalSum));
121-
// TODO add a label with current Heimat Time
122-
// TODO add a label with current Keeptime time
126+
LocalTime.ofSecondOfDay(
127+
items.stream().filter(item -> item.mapping.heimatTaskId() != -1L) // if its bookable in heimat
128+
.mapToLong(item -> item.userTimeSeconds.getValue()).sum())), items2);
129+
sumTimeLabel.textProperty().bind(totalSum);
130+
131+
keepTimeTimeLabel.setText(localTimeStringConverter.toString(
132+
LocalTime.ofSecondOfDay(tableRows.stream().mapToLong(HeimatController.Mapping::keeptimeSeconds).sum())));
133+
heimatTimeLabel.setText(localTimeStringConverter.toString(
134+
LocalTime.ofSecondOfDay(tableRows.stream().mapToLong(HeimatController.Mapping::heimatSeconds).sum())));
135+
136+
externalSystemLink.setOnAction(ae -> BrowserHelper.openURL(heimatController.getUrlForDay(currentReportDate)));
123137
}
124138

125139
@FXML
@@ -160,8 +174,6 @@ protected void updateItem(TableRow item, boolean empty) {
160174
TableColumn<TableRow, List<Project>> projectColumn = new TableColumn<>("Project");
161175
projectColumn.setCellValueFactory(data -> new SimpleObjectProperty(data.getValue().mapping.projects()));
162176
projectColumn.setCellFactory(column -> new TableCell<>() {
163-
//private final Label label = new Label();
164-
165177
@Override
166178
protected void updateItem(List<Project> item, boolean empty) {
167179
super.updateItem(item, empty);
@@ -222,13 +234,17 @@ protected void updateItem(TableRow item, boolean empty) {
222234
LocalTime.ofSecondOfDay(item.keeptimeTimeSeconds.get())));
223235
heimatLabel.setText("Heimat: " + localTimeStringConverter.toString(
224236
LocalTime.ofSecondOfDay(item.heimatTimeSeconds.get())));
225-
timeSpinner.getValueFactory().setValue(LocalTime.ofSecondOfDay(item.userTimeSeconds.get()));
226-
localTimeChangeListener = (observable, oldValue, newValue) -> {
227-
item.userTimeSeconds.set(newValue.toSecondOfDay());
237+
timeSpinner.setDisable(!item.mapping.canBeSynced());
238+
timeSpinner.getValueFactory().setValue(LocalTime.ofSecondOfDay(0));
239+
if (item.mapping.canBeSynced()) {
240+
timeSpinner.getValueFactory().setValue(LocalTime.ofSecondOfDay(item.userTimeSeconds.get()));
241+
localTimeChangeListener = (observable, oldValue, newValue) -> {
242+
item.userTimeSeconds.set(newValue.toSecondOfDay());
243+
spinnerValid.accept(timeSpinner);
244+
};
228245
spinnerValid.accept(timeSpinner);
229-
};
230-
spinnerValid.accept(timeSpinner);
231-
timeSpinner.valueProperty().addListener(localTimeChangeListener);
246+
timeSpinner.valueProperty().addListener(localTimeChangeListener);
247+
}
232248
setGraphic(container);
233249
}
234250
}
@@ -258,9 +274,25 @@ protected void updateItem(TableRow item, boolean empty) {
258274
textArea.setPrefHeight(50);
259275
textArea.setPrefWidth(100);
260276
textArea.setWrapText(true);
261-
// TODO make it possible to copy content of heimatNotesLabel
262-
hbox.getChildren().addAll(new Label("Keeptime:"), keepTimeNotesLabel);
263-
hbox2.getChildren().addAll(new Label("Heimat:"), heimatNotesLabel);
277+
278+
final Button copyKeepTimeNotes = new Button("",
279+
SvgNodeProvider.getSvgNodeWithScale(Resources.RESOURCE.SVG_CLIPBOARD_ICON, 0.03, 0.03));
280+
copyKeepTimeNotes.setMaxSize(20, 18);
281+
copyKeepTimeNotes.setMinSize(20, 18);
282+
copyKeepTimeNotes.setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
283+
copyKeepTimeNotes.setTooltip(new Tooltip("Copy notes"));
284+
copyKeepTimeNotes.setOnAction(me -> copyToClipboard(keepTimeNotesLabel.getText()));
285+
286+
final Button copyHeimatNotes = new Button("",
287+
SvgNodeProvider.getSvgNodeWithScale(Resources.RESOURCE.SVG_CLIPBOARD_ICON, 0.03, 0.03));
288+
copyHeimatNotes.setMaxSize(20, 18);
289+
copyHeimatNotes.setMinSize(20, 18);
290+
copyHeimatNotes.setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
291+
copyHeimatNotes.setTooltip(new Tooltip("Copy notes"));
292+
copyHeimatNotes.setOnAction(me -> copyToClipboard(heimatNotesLabel.getText()));
293+
294+
hbox.getChildren().addAll(copyKeepTimeNotes, new Label("KeepTime:"), keepTimeNotesLabel);
295+
hbox2.getChildren().addAll(copyHeimatNotes, new Label("Heimat:"), heimatNotesLabel);
264296
container.getChildren().addAll(textArea, hbox, hbox2);
265297
}
266298

@@ -272,13 +304,17 @@ protected void updateItem(TableRow item, boolean empty) {
272304
if (empty || item == null) {
273305
setGraphic(null);
274306
} else {
275-
textArea.setText(item.userNotes.get());
276-
stringChangeListener = (obs, oldText, newText) -> {
277-
item.userNotes.set(newText);
307+
textArea.setDisable(!item.mapping.canBeSynced());
308+
textArea.setText("");
309+
if (item.mapping.canBeSynced()) {
310+
textArea.setText(item.userNotes.get());
311+
stringChangeListener = (obs, oldText, newText) -> {
312+
item.userNotes.set(newText);
313+
textAreaValid.accept(textArea);
314+
};
278315
textAreaValid.accept(textArea);
279-
};
280-
textAreaValid.accept(textArea);
281-
textArea.textProperty().addListener(stringChangeListener);
316+
textArea.textProperty().addListener(stringChangeListener);
317+
}
282318
heimatNotesLabel.setText(item.heimatNotes.get());
283319
keepTimeNotesLabel.setText(item.keeptimeNotes.get());
284320
setGraphic(container);
@@ -291,6 +327,7 @@ protected void updateItem(TableRow item, boolean empty) {
291327
syncColumn.setCellValueFactory(data -> data.getValue().syncStatus);
292328
syncColumn.setPrefWidth(250);
293329
mappingTableView.getColumns().addAll(shouldSyncColumn, projectColumn, timeColumn, notesColumn, syncColumn);
330+
mappingTableView.setSelectionModel(null);
294331

295332
saveButton.setOnAction((ae) -> {
296333
LOG.debug("New mappings to be synced '{}'.", "TODO");

0 commit comments

Comments
 (0)