Skip to content

Commit 71e5ffb

Browse files
committed
#178: show times from heimat in sync view for conflict resolution. add round up/down functionality.
1 parent f8bef51 commit 71e5ffb

File tree

2 files changed

+131
-45
lines changed

2 files changed

+131
-45
lines changed

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

Lines changed: 128 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,21 @@
11
package de.doubleslash.keeptime.view;
22

3+
import de.doubleslash.keeptime.controller.Controller;
34
import de.doubleslash.keeptime.model.*;
45
import de.doubleslash.keeptime.model.repos.ExternalProjectsMappingsRepository;
56
import de.doubleslash.keeptime.model.settings.HeimatSettings;
67
import de.doubleslash.keeptime.rest.integration.heimat.HeimatAPI;
8+
import de.doubleslash.keeptime.rest.integration.heimat.model.HeimatTime;
79
import javafx.beans.binding.Bindings;
810
import javafx.beans.binding.StringBinding;
911
import javafx.beans.property.*;
12+
import javafx.beans.value.ChangeListener;
1013
import javafx.collections.FXCollections;
1114
import javafx.collections.ObservableList;
1215
import javafx.fxml.FXML;
1316
import javafx.scene.control.*;
1417
import javafx.scene.control.cell.CheckBoxTableCell;
18+
import javafx.scene.layout.HBox;
1519
import javafx.scene.layout.VBox;
1620
import javafx.util.converter.LocalTimeStringConverter;
1721
import org.slf4j.Logger;
@@ -20,9 +24,11 @@
2024

2125
import java.time.LocalDate;
2226
import java.time.LocalTime;
27+
import java.time.format.DateTimeFormatter;
2328
import java.time.format.DateTimeParseException;
2429
import java.time.format.FormatStyle;
25-
import java.util.List;
30+
import java.util.*;
31+
import java.util.stream.Collectors;
2632

2733
@Component
2834
public class ExternalProjectsSyncController {
@@ -47,54 +53,94 @@ public class ExternalProjectsSyncController {
4753
@FXML
4854
private Hyperlink externalSystemLink;
4955

56+
private final Controller controller;
5057
private final Model model;
5158
private final HeimatSettings heimatSettings;
5259
private final ExternalProjectsMappingsRepository externalProjectsMappingsRepository;
5360

54-
private final LocalTimeStringConverter localTimeStringConverter = new LocalTimeStringConverter(FormatStyle.SHORT);
61+
private final LocalTimeStringConverter localTimeStringConverter = new LocalTimeStringConverter(FormatStyle.MEDIUM);
5562

56-
public ExternalProjectsSyncController(final Model model, HeimatSettings heimatSettings,
63+
public ExternalProjectsSyncController(final Controller controller, final Model model, HeimatSettings heimatSettings,
5764
ExternalProjectsMappingsRepository externalProjectsMappingsRepository) {
65+
this.controller = controller;
5866
this.model = model;
5967
this.heimatSettings = heimatSettings;
6068
this.externalProjectsMappingsRepository = externalProjectsMappingsRepository;
6169
}
6270

63-
@FXML
64-
private void initialize() {
65-
// TODO set this from ReportController
66-
final LocalDate currentReportDate = LocalDate.now();
67-
final List<Work> currentWorkItems = model.getWorkRepository()
68-
.findByStartDateOrderByStartTimeAsc(currentReportDate);
71+
public void initForDate(LocalDate currentReportDate, List<Work> currentWorkItems) {
72+
dayOfSyncLabel.setText(currentReportDate.format(DateTimeFormatter.BASIC_ISO_DATE));
6973

7074
final HeimatAPI heimatAPI = new HeimatAPI(heimatSettings.getHeimatUrl(), heimatSettings.getHeimatPat());
71-
//final List<HeimatTask> externalProjects = heimatAPI.getMyTasks(currentReportDate);
75+
// TODO check if external projects are available for the currentDay
76+
// final List<HeimatTask> heimatTasks = heimatAPI.getMyTasks(currentReportDate);
77+
final List<HeimatTime> heimatTimes = heimatAPI.getMyTimes(currentReportDate);
7278
final List<ExternalProjectMapping> mappedProjects = externalProjectsMappingsRepository.findByExternalSystemId(
7379
ExternalSystem.Heimat);
74-
// TODO check if external projects are available for the currentDay
7580

76-
final TableRow tableRow = new TableRow();
77-
tableRow.project = model.activeWorkItem.get().getProject();
78-
tableRow.shouldSyncCheckBox = new SimpleBooleanProperty(true);
79-
tableRow.syncStatus = new SimpleStringProperty("Can be synced");
80-
tableRow.heimatNotes = new SimpleStringProperty("Heimat notes");
81-
tableRow.keeptimeNotes = new SimpleStringProperty("Current notes");
82-
tableRow.heimatTimeMinutes = new SimpleIntegerProperty(90);
83-
tableRow.userTimeMinutes = new SimpleIntegerProperty(90);
84-
tableRow.keeptimeTimeMinutes = new SimpleIntegerProperty(90);
85-
final ObservableList<TableRow> items = FXCollections.observableArrayList(tableRow);
81+
final List<TableRow> list = new ArrayList<>();
82+
83+
final SortedSet<Project> workedProjectsSet = currentWorkItems.stream()
84+
.map(Work::getProject)
85+
.filter(Project::isWork)
86+
.collect(Collectors.toCollection(() -> new TreeSet<>(
87+
Comparator.comparing(Project::getIndex))));
88+
for (final Project project : workedProjectsSet) {
89+
String heimatNotes = "";
90+
long heimatTimeSeconds = 0;
91+
boolean isMappedInHeimat = false;
92+
final Optional<ExternalProjectMapping> optionalHeimatMapping = mappedProjects.stream()
93+
.filter(mp -> mp.getProject()
94+
.getId()
95+
== project.getId())
96+
.findAny();
97+
if (optionalHeimatMapping.isPresent()) {
98+
isMappedInHeimat = true;
99+
final Optional<HeimatTime> optionalAlreadyBookedTime = heimatTimes.stream()
100+
.filter(heimatTime -> heimatTime.taskId()
101+
== optionalHeimatMapping.get()
102+
.getExternalTaskId())
103+
.findAny();
104+
if (optionalAlreadyBookedTime.isPresent()) {
105+
heimatNotes = optionalAlreadyBookedTime.get().note();
106+
heimatTimeSeconds = optionalAlreadyBookedTime.get().durationInMinutes() * 60L;
107+
}
108+
}
109+
final List<Work> onlyCurrentProjectWork = currentWorkItems.stream()
110+
.filter(w -> w.getProject() == project)
111+
.toList();
112+
113+
final long projectWorkSeconds = controller.calcSeconds(onlyCurrentProjectWork);
114+
115+
final ProjectReport pr = new ProjectReport();
116+
for (final Work work : onlyCurrentProjectWork) {
117+
final String currentWorkNote = work.getNotes();
118+
pr.appendToWorkNotes(currentWorkNote);
119+
}
120+
final String keeptimeNotes = pr.getNotes();
121+
String canBeSynced = "Can be synced";
122+
if (!isMappedInHeimat) {
123+
canBeSynced = "Not mapped in Heimat";
124+
}
125+
list.add(new TableRow(project, isMappedInHeimat, canBeSynced, heimatNotes, keeptimeNotes, keeptimeNotes, heimatTimeSeconds,
126+
projectWorkSeconds, projectWorkSeconds));
127+
}
128+
final ObservableList<TableRow> items = FXCollections.observableArrayList(list);
86129
mappingTableView.setItems(items);
87130

88131
ObservableList<TableRow> items2 = FXCollections.observableArrayList(
89-
item -> new javafx.beans.Observable[] { item.userTimeMinutes, item.shouldSyncCheckBox });
132+
item -> new javafx.beans.Observable[] { item.userTimeSeconds, item.shouldSyncCheckBox });
90133
items2.addAll(items);
91134
StringBinding totalSum = Bindings.createStringBinding(() -> localTimeStringConverter.toString(
92135
LocalTime.ofSecondOfDay(items.stream()
93136
.filter(item -> item.shouldSyncCheckBox.get())
94-
.mapToInt(item -> item.userTimeMinutes.getValue())
95-
.sum() * 60)), items2);
137+
.mapToLong(item -> item.userTimeSeconds.getValue())
138+
.sum())), items2);
96139
sumTimeLabel.textProperty().bind(Bindings.concat("Total Sum: ", totalSum));
140+
}
97141

142+
@FXML
143+
private void initialize() {
98144
TableColumn<TableRow, Boolean> shouldSyncColumn = new TableColumn<>("Sync");
99145
shouldSyncColumn.setCellValueFactory(data -> data.getValue().shouldSyncCheckBox);
100146
shouldSyncColumn.setCellFactory(CheckBoxTableCell.forTableColumn(shouldSyncColumn));
@@ -105,14 +151,15 @@ private void initialize() {
105151
TableColumn<TableRow, String> projectColumn = new TableColumn<>("Project");
106152
projectColumn.setCellValueFactory(data -> new SimpleStringProperty(data.getValue().project.getName()));
107153
projectColumn.setPrefWidth(100);
154+
// TODO set color
108155
TableColumn<TableRow, TableRow> timeColumn = new TableColumn<>("Time");
109156
timeColumn.setCellValueFactory(data -> new SimpleObjectProperty<>(data.getValue())); // Placeholder property
110157

111158
timeColumn.setCellFactory(column -> new TableCell<>() {
112159
private final Spinner<LocalTime> timeSpinner = new Spinner<>();
113160
private final Label keeptimeLabel = new Label();
114161
private final Label heimatLabel = new Label();
115-
162+
private ChangeListener<LocalTime> localTimeChangeListener;
116163
private final VBox container = new VBox(5); // Space between TextArea and Label
117164

118165
{
@@ -123,18 +170,20 @@ private void initialize() {
123170
@Override
124171
protected void updateItem(TableRow item, boolean empty) {
125172
super.updateItem(item, empty);
126-
173+
if (localTimeChangeListener != null)
174+
timeSpinner.valueProperty().removeListener(localTimeChangeListener);
127175
if (empty || item == null) {
128176
setGraphic(null);
129177
} else {
130178
keeptimeLabel.setText("KeepTime: " + localTimeStringConverter.toString(
131-
LocalTime.ofSecondOfDay(item.keeptimeTimeMinutes.get() * 60)));
179+
LocalTime.ofSecondOfDay(item.keeptimeTimeSeconds.get())));
132180
heimatLabel.setText("Heimat: " + localTimeStringConverter.toString(
133-
LocalTime.ofSecondOfDay(item.heimatTimeMinutes.get() * 60)));
134-
timeSpinner.getValueFactory().setValue(LocalTime.ofSecondOfDay(item.userTimeMinutes.get() * 60));
135-
timeSpinner.valueProperty().addListener((observable, oldValue, newValue) -> {
136-
item.userTimeMinutes.set(newValue.getHour() * 60 + newValue.getMinute());
137-
});
181+
LocalTime.ofSecondOfDay(item.heimatTimeSeconds.get())));
182+
timeSpinner.getValueFactory().setValue(LocalTime.ofSecondOfDay(item.userTimeSeconds.get()));
183+
localTimeChangeListener = (observable, oldValue, newValue) -> {
184+
item.userTimeSeconds.set(newValue.toSecondOfDay());
185+
};
186+
timeSpinner.valueProperty().addListener(localTimeChangeListener);
138187
setGraphic(container);
139188
}
140189
}
@@ -145,27 +194,32 @@ protected void updateItem(TableRow item, boolean empty) {
145194
notesColumn.setCellValueFactory(data -> new SimpleObjectProperty<>(data.getValue())); // Placeholder property
146195

147196
notesColumn.setCellFactory(column -> new TableCell<>() {
197+
private ChangeListener<String> stringChangeListener;
148198
private final TextArea textArea = new TextArea();
149-
private final Label label = new Label();
199+
private final Label heimatNotesLabel = new Label();
200+
private final HBox hbox = new HBox(5);
150201
private final VBox container = new VBox(5); // Space between TextArea and Label
151202

152203
{
153204
textArea.setPrefHeight(50);
154205
textArea.setPrefWidth(100);
155206
textArea.setWrapText(true);
156-
container.getChildren().addAll(textArea, label);
207+
hbox.getChildren().addAll(new Label("Heimat:"), heimatNotesLabel);
208+
container.getChildren().addAll(textArea, hbox);
157209
}
158210

159211
@Override
160212
protected void updateItem(TableRow item, boolean empty) {
161213
super.updateItem(item, empty);
162-
214+
if (stringChangeListener != null)
215+
textArea.textProperty().removeListener(stringChangeListener);
163216
if (empty || item == null) {
164217
setGraphic(null);
165218
} else {
166219
textArea.setText(item.keeptimeNotes.get());
167-
textArea.textProperty().addListener((obs, oldText, newText) -> item.keeptimeNotes.set(newText));
168-
label.setText(item.heimatNotes.get());
220+
stringChangeListener = (obs, oldText, newText) -> item.keeptimeNotes.set(newText);
221+
textArea.textProperty().addListener(stringChangeListener);
222+
heimatNotesLabel.setText(item.heimatNotes.get());
169223
setGraphic(container);
170224
}
171225
}
@@ -207,8 +261,10 @@ public void decrement(final int steps) {
207261
if (getValue() == null) {
208262
setValue(LocalTime.now());
209263
} else {
264+
if (steps == 0)
265+
return;
210266
final LocalTime time = getValue();
211-
setValue(time.minusMinutes(15 * steps));
267+
setValue(decrementToLastFullQuarter(time));
212268
}
213269

214270
}
@@ -218,31 +274,59 @@ public void increment(final int steps) {
218274
if (getValue() == null) {
219275
setValue(LocalTime.now());
220276
} else {
277+
if (steps == 0)
278+
return;
221279
final LocalTime time = getValue();
222-
setValue(time.plusMinutes(15 * steps));
280+
setValue(incrementToNextFullQuarter(time));
223281
}
224282

225283
}
226284

227285
});
228286

229-
spinner.getValueFactory().setConverter(new LocalTimeStringConverter(FormatStyle.SHORT));
287+
spinner.getValueFactory().setConverter(new LocalTimeStringConverter(FormatStyle.MEDIUM));
288+
289+
// TODO mark red if not a 15 minute slot
290+
}
291+
292+
public static LocalTime decrementToLastFullQuarter(LocalTime time) {
293+
int minutes = time.getMinute();
294+
int decrement = (minutes % 15 == 0 && time.getSecond() == 0) ? 15 : minutes % 15;
295+
return time.minusMinutes(decrement).withSecond(0).withNano(0);
296+
}
230297

298+
public static LocalTime incrementToNextFullQuarter(LocalTime time) {
299+
int minutes = time.getMinute();
300+
int increment = (minutes % 15 == 0 && time.getSecond() == 0) ? 15 : 15 - (minutes % 15);
301+
return time.plusMinutes(increment).withSecond(0).withNano(0);
231302
}
232303

233-
class TableRow {
304+
static class TableRow {
234305
public BooleanProperty shouldSyncCheckBox;
235306
public Project project;
236307

237308
public StringProperty keeptimeNotes;
238309
public StringProperty userNotes;
239310
public StringProperty heimatNotes;
240311

241-
public IntegerProperty keeptimeTimeMinutes;
242-
public IntegerProperty userTimeMinutes;
243-
public IntegerProperty heimatTimeMinutes;
312+
public LongProperty keeptimeTimeSeconds;
313+
public LongProperty userTimeSeconds;
314+
public LongProperty heimatTimeSeconds;
244315

245316
public StringProperty syncStatus;
246317

318+
public TableRow(final Project project, final boolean shouldSync, final String syncStatus,
319+
final String heimatNotes, final String keeptimeNotes, String userNotes, final long heimatTimeSeconds,
320+
final long keeptimeSeconds, final long userSeconds) {
321+
this.project = project;
322+
this.shouldSyncCheckBox = new SimpleBooleanProperty(shouldSync);
323+
this.syncStatus = new SimpleStringProperty(syncStatus);
324+
this.heimatNotes = new SimpleStringProperty(heimatNotes);
325+
this.keeptimeNotes = new SimpleStringProperty(keeptimeNotes);
326+
this.userNotes = new SimpleStringProperty(userNotes);
327+
this.heimatTimeSeconds = new SimpleLongProperty(heimatTimeSeconds);
328+
this.userTimeSeconds = new SimpleLongProperty(userSeconds);
329+
this.keeptimeTimeSeconds = new SimpleLongProperty(keeptimeSeconds);
330+
}
247331
}
248332
}

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@ public class ReportController {
118118
private final TreeItem<TableRow> rootItem = new TreeItem<>();
119119

120120
private boolean expanded = true;
121+
private List<Work> currentWorkItems;
121122

122123
public ReportController(final Model model, final Controller controller) {
123124
this.model = model;
@@ -154,6 +155,7 @@ private void showSyncStage() throws IOException {
154155
fxmlLoader2.setControllerFactory(model.getSpringContext()::getBean);
155156
final Parent settingsRoot = fxmlLoader2.load();
156157
ExternalProjectsSyncController settingsController = fxmlLoader2.getController();
158+
settingsController.initForDate(currentReportDate, currentWorkItems);
157159
Stage settingsStage = new Stage();
158160
//settingsController.setStage(settingsStage);
159161
settingsStage.initModality(Modality.APPLICATION_MODAL);
@@ -289,7 +291,7 @@ private void updateReport(final LocalDate dateToShow) {
289291
reportRoot.requestFocus();
290292

291293
this.currentDayLabel.setText(DateFormatter.toDayDateString(this.currentReportDate));
292-
final List<Work> currentWorkItems = model.getWorkRepository()
294+
currentWorkItems = model.getWorkRepository()
293295
.findByStartDateOrderByStartTimeAsc(this.currentReportDate);
294296

295297
colorTimeLine.update(currentWorkItems, controller.calcSeconds(currentWorkItems));

0 commit comments

Comments
 (0)