implemented joining of times
This commit is contained in:
@@ -68,10 +68,20 @@ public class Time implements Mappable{
|
||||
return description;
|
||||
}
|
||||
|
||||
public Time description(String newVal) {
|
||||
description = newVal;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Long end(){
|
||||
return end;
|
||||
}
|
||||
|
||||
public Time end(Long newVal) {
|
||||
end = newVal;
|
||||
return this;
|
||||
}
|
||||
|
||||
public long id(){
|
||||
return id;
|
||||
}
|
||||
@@ -143,6 +153,11 @@ public class Time implements Mappable{
|
||||
return subject;
|
||||
}
|
||||
|
||||
public Time subject(String newValue) {
|
||||
subject = newValue;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Collection<Long> taskIds(){
|
||||
return taskIds;
|
||||
}
|
||||
|
||||
@@ -59,6 +59,26 @@
|
||||
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(){
|
||||
const url = api('time');
|
||||
@@ -189,6 +209,9 @@
|
||||
{:else}
|
||||
<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}
|
||||
{#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 class="duration" onclick={e => {detail = time.id}}>
|
||||
{#if time.duration}
|
||||
|
||||
@@ -7,7 +7,7 @@ public class Constants {
|
||||
|
||||
public static final String CHILDREN = "children";
|
||||
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 TABLE_TASK_TIMES = "task_times";
|
||||
public static final String TABLE_TIMES = "times";
|
||||
|
||||
@@ -6,6 +6,7 @@ import static de.srsoftware.umbrella.core.Constants.*;
|
||||
import static de.srsoftware.umbrella.core.Paths.*;
|
||||
import static de.srsoftware.umbrella.core.Util.mapValues;
|
||||
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.time.Constants.*;
|
||||
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
|
||||
public boolean doPatch(Path path, HttpExchange ex) throws IOException {
|
||||
addCors(ex);
|
||||
@@ -114,6 +103,7 @@ public class TimeModule extends BaseHandler implements TimeService {
|
||||
if (user.isEmpty()) return unauthorized(ex);
|
||||
var head = path.pop();
|
||||
return switch (head) {
|
||||
case JOIN -> joinTimes(ex,user.get());
|
||||
case LIST -> listTimes(ex,user.get());
|
||||
case TRACK_TASK -> trackTask(user.get(),path,ex);
|
||||
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()
|
||||
.stream()
|
||||
.filter(time -> time.state() == Started)
|
||||
.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 {
|
||||
var time = timeDb.load(timeId);
|
||||
if (time.userId() != user.id()) throw forbidden("You are not allowed to alter this time!");
|
||||
|
||||
@@ -100,6 +100,7 @@
|
||||
"invoice": "Rechnung",
|
||||
"items": "Artikel",
|
||||
|
||||
"join_objects" : "{objects} zusammenführen",
|
||||
"key": "Suchbegriff",
|
||||
|
||||
"language": "Sprache",
|
||||
@@ -237,7 +238,7 @@
|
||||
"tax_rate": "Steuersatz",
|
||||
"template": "Vorlage",
|
||||
"theme": "Design",
|
||||
"times": "Zeiterfassung",
|
||||
"times": "Zeiten",
|
||||
"timetracking": "Zeiterfassung",
|
||||
"title_or_desc": "Titel/Beschreibung",
|
||||
"tutorial": "Tutorial",
|
||||
|
||||
@@ -344,4 +344,17 @@ li > a > p:nth-child(1){
|
||||
}
|
||||
table{
|
||||
width: 100vw;
|
||||
}
|
||||
|
||||
.start_end{
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.start_end button.join{
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: -12px;
|
||||
border: 0 none;
|
||||
background: none;
|
||||
color: orange;
|
||||
}
|
||||
Reference in New Issue
Block a user