Browse Source

implemented joining of times

feature/join_times
Stephan Richter 2 months ago
parent
commit
45c2f3978f
  1. 15
      core/src/main/java/de/srsoftware/umbrella/core/model/Time.java
  2. 23
      frontend/src/routes/time/Index.svelte
  3. 2
      time/src/main/java/de/srsoftware/umbrella/time/Constants.java
  4. 70
      time/src/main/java/de/srsoftware/umbrella/time/TimeModule.java
  5. 3
      translations/src/main/resources/de.json
  6. 13
      web/src/main/resources/web/css/default.css

15
core/src/main/java/de/srsoftware/umbrella/core/model/Time.java

@ -68,10 +68,20 @@ public class Time implements Mappable{
return description; return description;
} }
public Time description(String newVal) {
description = newVal;
return this;
}
public Long end(){ public Long end(){
return end; return end;
} }
public Time end(Long newVal) {
end = newVal;
return this;
}
public long id(){ public long id(){
return id; return id;
} }
@ -143,6 +153,11 @@ public class Time implements Mappable{
return subject; return subject;
} }
public Time subject(String newValue) {
subject = newValue;
return this;
}
public Collection<Long> taskIds(){ public Collection<Long> taskIds(){
return taskIds; return taskIds;
} }

23
frontend/src/routes/time/Index.svelte

@ -59,6 +59,26 @@
return result; return result;
} }
async function joinTimes(evt, id1, id2){
evt.preventDefault();
evt.stopPropagation();
const url = api('time/join');
const res = await fetch(url,{
credentials : 'include',
method : 'POST',
body : `${id1}+${id2}`
});
if (res.ok){
error = null;
let json = await res.json();
delete times[id1];
delete times[id2];
times[json.id] = json;
} else {
error = await res.text();
}
return false;
}
async function loadTimes(){ async function loadTimes(){
const url = api('time'); const url = api('time');
@ -189,6 +209,9 @@
{:else} {:else}
<td class="start_end" onclick={e => toggleSelect(time.id)}> <td class="start_end" onclick={e => toggleSelect(time.id)}>
{time.start}{#if time.end_time}<wbr><wbr>{time.start.startsWith(time.end_date)?time.end.substring(11):time.end}{/if} {time.start}{#if time.end_time}<wbr><wbr>{time.start.startsWith(time.end_date)?time.end.substring(11):time.end}{/if}
{#if line>0 && Math.abs(sortedTimes[line-1].start_time - time.end_time)<100}
<button class="symbol join" title={t('join_objects',{objects:t('times')})} onclick={e => joinTimes(e, time.id, sortedTimes[line-1].id)} ></button>
{/if}
</td> </td>
<td class="duration" onclick={e => {detail = time.id}}> <td class="duration" onclick={e => {detail = time.id}}>
{#if time.duration} {#if time.duration}

2
time/src/main/java/de/srsoftware/umbrella/time/Constants.java

@ -7,7 +7,7 @@ public class Constants {
public static final String CHILDREN = "children"; public static final String CHILDREN = "children";
public static final String CONFIG_DATABASE = "umbrella.modules.time.database"; public static final String CONFIG_DATABASE = "umbrella.modules.time.database";
public static final String JOIN = "join";
public static final String PROJECTS = "projects"; public static final String PROJECTS = "projects";
public static final String TABLE_TASK_TIMES = "task_times"; public static final String TABLE_TASK_TIMES = "task_times";
public static final String TABLE_TIMES = "times"; public static final String TABLE_TIMES = "times";

70
time/src/main/java/de/srsoftware/umbrella/time/TimeModule.java

@ -6,6 +6,7 @@ import static de.srsoftware.umbrella.core.Constants.*;
import static de.srsoftware.umbrella.core.Paths.*; import static de.srsoftware.umbrella.core.Paths.*;
import static de.srsoftware.umbrella.core.Util.mapValues; import static de.srsoftware.umbrella.core.Util.mapValues;
import static de.srsoftware.umbrella.core.exceptions.UmbrellaException.*; import static de.srsoftware.umbrella.core.exceptions.UmbrellaException.*;
import static de.srsoftware.umbrella.core.model.Time.State.Open;
import static de.srsoftware.umbrella.core.model.Time.State.Started; import static de.srsoftware.umbrella.core.model.Time.State.Started;
import static de.srsoftware.umbrella.time.Constants.*; import static de.srsoftware.umbrella.time.Constants.*;
import static java.util.stream.Collectors.toSet; import static java.util.stream.Collectors.toSet;
@ -76,18 +77,6 @@ public class TimeModule extends BaseHandler implements TimeService {
} }
} }
private boolean getStoppedTask(UmbrellaUser user, long timeId, HttpExchange ex) throws IOException {
long now;
try {
now = Long.parseLong(body(ex));
} catch (NumberFormatException e) {
throw unprocessable("request body does not contain a timestamp!");
}
var time = timeDb.load(timeId);
timeDb.save(time.stop(now));
return sendContent(ex,time);
}
@Override @Override
public boolean doPatch(Path path, HttpExchange ex) throws IOException { public boolean doPatch(Path path, HttpExchange ex) throws IOException {
addCors(ex); addCors(ex);
@ -114,6 +103,7 @@ public class TimeModule extends BaseHandler implements TimeService {
if (user.isEmpty()) return unauthorized(ex); if (user.isEmpty()) return unauthorized(ex);
var head = path.pop(); var head = path.pop();
return switch (head) { return switch (head) {
case JOIN -> joinTimes(ex,user.get());
case LIST -> listTimes(ex,user.get()); case LIST -> listTimes(ex,user.get());
case TRACK_TASK -> trackTask(user.get(),path,ex); case TRACK_TASK -> trackTask(user.get(),path,ex);
default -> { default -> {
@ -129,13 +119,67 @@ public class TimeModule extends BaseHandler implements TimeService {
} }
} }
private Optional<Time> getStartedTime(UmbrellaUser user){ private boolean joinTimes(HttpExchange ex, UmbrellaUser user) throws IOException {
var parts = body(ex).split("\\+");
if (parts.length != 2) throw unprocessable("Expected two time ids as body");
long id1, id2;
try {
id1 = Long.parseLong(parts[0]);
id2 = Long.parseLong(parts[1]);
} catch (NumberFormatException e) {
throw unprocessable("Expected two time ids as body");
}
var time1 = timeDb.load(id1);
if (time1.userId() != user.id()) throw forbidden("You are not owner of time {0}",time1.id());
if (time1.state() != Open) throw forbidden("Time is not editable");
var time2 = timeDb.load(id2);
if (time2.userId() != user.id()) throw forbidden("You are not owner of time {0}",time2.id());
if (time2.state() != Open) throw forbidden("Time is not editable");
if (time1.start() > time2.start()) {
var dummy = time1;
time1 = time2;
time2 = dummy;
}
if (Math.abs(time1.end() - time2.start())>100L) throw unprocessable("Times are not adjacent");
var subject = time1.subject();
if (time2.subject().contains(subject)) {
subject = time2.subject();
} else if (!subject.contains(time2.subject())) subject += " / "+time2.subject();
var description = time1.description();
if (time2.description().contains(description)) {
description = time2.description();
} else if (!description.contains(time2.description())) description += "\n"+time2.description();
time1.end(time2.end());
time1.taskIds().addAll(time2.taskIds());
time1.subject(subject);
time1.description(description);
var result = timeDb.save(time1);
timeDb.delete(time2.id());
return sendContent(ex,result);
}
private Optional<Time> getStartedTime(UmbrellaUser user){
return timeDb.listUserTimes(user.id(), false).values() return timeDb.listUserTimes(user.id(), false).values()
.stream() .stream()
.filter(time -> time.state() == Started) .filter(time -> time.state() == Started)
.max(Comparator.comparing(Time::start)); .max(Comparator.comparing(Time::start));
} }
private boolean getStoppedTask(UmbrellaUser user, long timeId, HttpExchange ex) throws IOException {
long now;
try {
now = Long.parseLong(body(ex));
} catch (NumberFormatException e) {
throw unprocessable("request body does not contain a timestamp!");
}
var time = timeDb.load(timeId);
timeDb.save(time.stop(now));
return sendContent(ex,time);
}
private boolean patchTime(UmbrellaUser user, long timeId, HttpExchange ex) throws IOException { private boolean patchTime(UmbrellaUser user, long timeId, HttpExchange ex) throws IOException {
var time = timeDb.load(timeId); var time = timeDb.load(timeId);
if (time.userId() != user.id()) throw forbidden("You are not allowed to alter this time!"); if (time.userId() != user.id()) throw forbidden("You are not allowed to alter this time!");

3
translations/src/main/resources/de.json

@ -100,6 +100,7 @@
"invoice": "Rechnung", "invoice": "Rechnung",
"items": "Artikel", "items": "Artikel",
"join_objects" : "{objects} zusammenführen",
"key": "Suchbegriff", "key": "Suchbegriff",
"language": "Sprache", "language": "Sprache",
@ -237,7 +238,7 @@
"tax_rate": "Steuersatz", "tax_rate": "Steuersatz",
"template": "Vorlage", "template": "Vorlage",
"theme": "Design", "theme": "Design",
"times": "Zeiterfassung", "times": "Zeiten",
"timetracking": "Zeiterfassung", "timetracking": "Zeiterfassung",
"title_or_desc": "Titel/Beschreibung", "title_or_desc": "Titel/Beschreibung",
"tutorial": "Tutorial", "tutorial": "Tutorial",

13
web/src/main/resources/web/css/default.css

@ -344,4 +344,17 @@ li > a > p:nth-child(1){
} }
table{ table{
width: 100vw; width: 100vw;
}
.start_end{
position: relative;
}
.start_end button.join{
position: absolute;
right: 0;
top: -12px;
border: 0 none;
background: none;
color: orange;
} }
Loading…
Cancel
Save