Browse Source

preparing for adding positions to document

feature/document
Stephan Richter 4 months ago
parent
commit
e436f09698
  1. 1
      core/src/main/java/de/srsoftware/umbrella/core/Constants.java
  2. 3
      core/src/main/java/de/srsoftware/umbrella/core/api/TaskService.java
  3. 46
      core/src/main/java/de/srsoftware/umbrella/core/model/Time.java
  4. 1
      documents/src/main/java/de/srsoftware/umbrella/documents/Constants.java
  5. 23
      documents/src/main/java/de/srsoftware/umbrella/documents/DocumentApi.java
  6. 45
      frontend/src/routes/document/PositionSelector.svelte
  7. 35
      frontend/src/routes/document/TimeList.svelte
  8. 19
      frontend/src/routes/document/View.svelte
  9. 3
      project/src/main/java/de/srsoftware/umbrella/project/ProjectModule.java
  10. 5
      task/src/main/java/de/srsoftware/umbrella/task/TaskModule.java
  11. 1
      time/src/main/java/de/srsoftware/umbrella/time/SqliteDb.java
  12. 1
      time/src/main/java/de/srsoftware/umbrella/time/TimeDb.java
  13. 71
      time/src/main/java/de/srsoftware/umbrella/time/TimeModule.java
  14. 5
      translations/src/main/resources/de.json

1
core/src/main/java/de/srsoftware/umbrella/core/Constants.java

@ -20,6 +20,7 @@ public class Constants { @@ -20,6 +20,7 @@ public class Constants {
public static final String DESCRIPTION = "description";
public static final String DOMAIN = "domain";
public static final String DUE_DATE = "due_date";
public static final String DURATION = "duration";
public static final String EMAIL = "email";
public static final String END_TIME = "end_time";

3
core/src/main/java/de/srsoftware/umbrella/core/api/TaskService.java

@ -8,8 +8,9 @@ import java.util.Collection; @@ -8,8 +8,9 @@ import java.util.Collection;
public interface TaskService {
CompanyService companyService();
Collection<Task> listCompanyTasks(long companyId) throws UmbrellaException;
Collection<Task> listProjectTasks(long projectId) throws UmbrellaException;
ProjectService projectService();
UserService userService();
}

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

@ -1,13 +1,14 @@ @@ -1,13 +1,14 @@
/* © SRSoftware 2025 */
package de.srsoftware.umbrella.core.model;
import de.srsoftware.tools.Mappable;
import static de.srsoftware.umbrella.core.Constants.*;
import static de.srsoftware.umbrella.core.Util.dateTimeOf;
import static de.srsoftware.umbrella.core.Util.markdown;
import de.srsoftware.tools.Mappable;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.time.Duration;
import java.time.LocalDateTime;
import java.util.*;
@ -59,18 +60,15 @@ public class Time implements Mappable{ @@ -59,18 +60,15 @@ public class Time implements Mappable{
this.taskIds = taskIds;
}
public long id(){
return id;
}
@Override
public Map<String, Object> toMap() {
var map = new HashMap<String,Object>();
map.put(ID,id);
map.put(USER_ID,userId);
map.put(SUBJECT,subject);
map.put(DESCRIPTION,description);
map.put(START_TIME,start);
map.put(END_TIME,end);
map.put(STATE,Map.of(STATUS_CODE,state.code,NAME,state.name()));
return map;
public boolean isClosed() {
return switch (state){
case Complete, Cancelled -> true;
case null, default -> false;
};
}
public static Time of(ResultSet rs) throws SQLException {
@ -90,4 +88,26 @@ public class Time implements Mappable{ @@ -90,4 +88,26 @@ public class Time implements Mappable{
new HashSet<>()
);
}
public LocalDateTime start(){
return start;
}
public Collection<Long> taskIds(){
return taskIds;
}
@Override
public Map<String, Object> toMap() {
var map = new HashMap<String,Object>();
map.put(ID,id);
map.put(USER_ID,userId);
map.put(SUBJECT,subject);
map.put(DESCRIPTION,Map.of(SOURCE,description,RENDERED,markdown(description)));
map.put(START_TIME,start.toString().replace("T"," "));
map.put(END_TIME,end.toString().replace("T"," "));
map.put(STATE,Map.of(STATUS_CODE,state.code,NAME,state.name()));
map.put(DURATION,Duration.between(start,end).toMinutes()/60d);
return map;
}
}

1
documents/src/main/java/de/srsoftware/umbrella/documents/Constants.java

@ -79,6 +79,7 @@ public class Constants { @@ -79,6 +79,7 @@ public class Constants {
public static final String PATH_POSITIONS = "positions";
public static final String PATH_SEND = "send";
public static final String PATH_TYPES = "types";
public static final String POSITION = "position";
public static final String PROJECT_ID = "project_id";
public static final String STATES = "states";

23
documents/src/main/java/de/srsoftware/umbrella/documents/DocumentApi.java

@ -141,7 +141,15 @@ public class DocumentApi extends BaseHandler { @@ -141,7 +141,15 @@ public class DocumentApi extends BaseHandler {
case LIST -> listCompaniesDocuments(ex,user.get(),token.orElse(null));
case TEMPLATES -> postTemplateList(ex,user.get());
case null -> postDocument(ex,user.get());
default -> super.doPost(path,ex);
default -> {
var docId = 0L;
try {
docId = Long.parseLong(head);
} catch (NumberFormatException ignored) {
yield super.doPost(path,ex);
}
yield postToDocument(ex,path,user.get(),docId);
}
};
} catch (UmbrellaException e) {
return send(ex,e);
@ -247,6 +255,10 @@ public class DocumentApi extends BaseHandler { @@ -247,6 +255,10 @@ public class DocumentApi extends BaseHandler {
return sendContent(ex,saved.toMap());
}
private boolean postDocumentPosition(long docId, HttpExchange ex, UmbrellaUser user) throws IOException {
return notImplemented(ex,"postDocumentPosition",this);
}
private boolean postTemplateList(HttpExchange ex, UmbrellaUser user) throws UmbrellaException, IOException {
var json = json(ex);
if (!(json.has(COMPANY) && json.get(COMPANY) instanceof Number companyId)) throw missingFieldException(COMPANY);
@ -255,4 +267,13 @@ public class DocumentApi extends BaseHandler { @@ -255,4 +267,13 @@ public class DocumentApi extends BaseHandler {
var templates = db.getCompanyTemplates(companyId.longValue());
return sendContent(ex,templates.stream().map(Template::toMap));
}
private boolean postToDocument(HttpExchange ex, Path path, UmbrellaUser user, long docId) throws IOException {
var head = path.pop();
return switch (head){
case POSITION -> postDocumentPosition(docId,ex,user);
case null, default -> super.doPost(path,ex);
};
}
}

45
frontend/src/routes/document/PositionSelector.svelte

@ -6,20 +6,43 @@ @@ -6,20 +6,43 @@
let { close = () => {}, doc = $bindable({}), onSelect = (item) => {} } = $props();
let select = $state(0);
let source = $state(0);
function estimateSelected(estimate){
onSelect({task:estimate});
function select(position){
close();
onSelect(position);
}
function estimateSelected(estimate){
select({
code:t('estimated_time'),
subject:estimate.name,
description:estimate.description.source,
amount:estimate.estimated_time,
unit:doc.currency+"/h"
});
}
function itemSelected(item){
onSelect({item:item});
close();
select({
code:item.code,
subject:item.name,
description:item.description.source,
amount:1,
unit:item.unit,
unit_price:item.unit_price,
tax:item.tax
});
}
function timeSelected(time){
console.log({timeSelected:time});
select({
code:t('document.timetrack'),
title:time.subject,
description:time.description.source,
amount:time.duration,
unit:doc.currency+"/h"
});
}
</script>
@ -42,14 +65,14 @@ @@ -42,14 +65,14 @@
<div class="position_selector">
<span class="tabs">
<button onclick={() => select=0}>{t('document.items')}</button>
<button onclick={() => select=1}>{t('document.estimated_times')}</button>
<button onclick={() => select=2}>{t('document.timetrack')}</button>
<button onclick={() => source=0}>{t('document.items')}</button>
<button onclick={() => source=1}>{t('document.estimated_times')}</button>
<button onclick={() => source=2}>{t('document.timetrack')}</button>
<button onclick={close}>{t('document.abort')}</button>
</span>
{#if select == 0}
{#if source == 0}
<ItemList company_id={doc.company.id} onSelect={itemSelected} />
{:else if select == 1}
{:else if source == 1}
<EstimateList company_id={doc.company.id} onSelect={estimateSelected} />
{:else}
<TimeList company_id={doc.company.id} onSelect={timeSelected} />

35
frontend/src/routes/document/TimeList.svelte

@ -4,17 +4,33 @@ @@ -4,17 +4,33 @@
let { company_id, onSelect = (time) => {} } = $props();
let projects = $state(null);
let times = $state(null);
let error = $state(null);
async function loadTimes(){
const url = `${location.protocol}//${location.host.replace('5173','8080')}/api/times/list`;
async function loadProjects(){
const url = `${location.protocol}//${location.host.replace('5173','8080')}/api/project/list`;
let data = { company_id: company_id };
const resp = await fetch(url,{
credentials:'include',
method: 'POST',
body: JSON.stringify(data)
});
if (resp.ok){
projects = await resp.json();
} else {
error = await resp.body();
}
}
async function loadTimes(projectId){
const url = `${location.protocol}//${location.host.replace('5173','8080')}/api/times/list`;
let data = { company_id: company_id, project_id: projectId };
const resp = await fetch(url,{
credentials:'include',
method: 'POST',
body: JSON.stringify(data)
});
if (resp.ok){
times = await resp.json();
} else {
@ -22,11 +38,24 @@ @@ -22,11 +38,24 @@
}
}
onMount(loadTimes);
onMount(loadProjects);
</script>
<div>
<h1>Times</h1>
{#if projects}
{#each projects as project,idx1}
<button onclick={() => loadTimes(project.id)}>{project.name}</button>
{/each}
{/if}
{#if times}
{#each times as time,idx2}
<div class="time" onclick={() => onSelect(time)}>
<span class="duration">{(time.duration).toFixed(3)} {t('hours')}</span>
<span class="subject">{time.subject}</span>
<span class="start_time">{time.start_time}</span>
<span class="description">{@html time.description.rendered}</span>
</div>
{/each}
{/if}
</div>

19
frontend/src/routes/document/View.svelte

@ -10,7 +10,7 @@ @@ -10,7 +10,7 @@
import StateSelector from './StateSelector.svelte';
import TemplateSelector from './TemplateSelector.svelte';
let { id } = $props();
let error = null;
let error = $state(null);
let doc = $state(null);
let position_select = $state(false);
@ -63,12 +63,17 @@ @@ -63,12 +63,17 @@
}
}
function addPosition(selected){
console.log(selected);
let newPos = {};
if (selected.item) newPos['item']=selected.item.id;
if (selected.task) newPos['task']=selected.task.id;
console.log(JSON.stringify({newPos:newPos}));
async function addPosition(selected){
const url = `${location.protocol}//${location.host.replace('5173','8080')}/api/document/${doc.id}/position`;
const resp = await fetch(url,{
method: 'POST',
credentials:'include',
body:JSON.stringify(selected)
});
if (resp.ok){
} else {
error = await resp.text();
}
}
onMount(loadDoc);

3
project/src/main/java/de/srsoftware/umbrella/project/ProjectModule.java

@ -7,6 +7,7 @@ import static de.srsoftware.umbrella.core.Paths.LIST; @@ -7,6 +7,7 @@ import static de.srsoftware.umbrella.core.Paths.LIST;
import static de.srsoftware.umbrella.core.exceptions.UmbrellaException.forbidden;
import static de.srsoftware.umbrella.core.exceptions.UmbrellaException.missingFieldException;
import static de.srsoftware.umbrella.project.Constants.CONFIG_DATABASE;
import static java.util.Comparator.comparing;
import com.sun.net.httpserver.HttpExchange;
import de.srsoftware.configuration.Configuration;
@ -61,7 +62,7 @@ public class ProjectModule extends BaseHandler implements ProjectService { @@ -61,7 +62,7 @@ public class ProjectModule extends BaseHandler implements ProjectService {
}
public Collection<Project> listProjects(long companyId, boolean includeClosed) throws UmbrellaException {
return projectDb.list(companyId, includeClosed);
return projectDb.list(companyId, includeClosed).stream().sorted(comparing(Project::name)).toList();
}
private boolean listItems(HttpExchange ex, UmbrellaUser user) throws IOException, UmbrellaException {

5
task/src/main/java/de/srsoftware/umbrella/task/TaskModule.java

@ -93,6 +93,11 @@ public class TaskModule extends BaseHandler implements TaskService { @@ -93,6 +93,11 @@ public class TaskModule extends BaseHandler implements TaskService {
return taskDb.listTasks(projectList.stream().map(Project::id).toList());
}
@Override
public Collection<Task> listProjectTasks(long projectId) throws UmbrellaException {
return taskDb.listTasks(List.of(projectId));
}
private Map<String,Object> placeInTree(Task task, HashMap<Long, Map<String,Object>> tree, Map<Long, Task> map) {
var taskMap = task.toMap();
if (task.parentTaskId() != null){

1
time/src/main/java/de/srsoftware/umbrella/time/SqliteDb.java

@ -13,7 +13,6 @@ import java.sql.SQLException; @@ -13,7 +13,6 @@ import java.sql.SQLException;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
public class SqliteDb implements TimeDb {

1
time/src/main/java/de/srsoftware/umbrella/time/TimeDb.java

@ -4,7 +4,6 @@ package de.srsoftware.umbrella.time; @@ -4,7 +4,6 @@ package de.srsoftware.umbrella.time;
import de.srsoftware.umbrella.core.exceptions.UmbrellaException;
import de.srsoftware.umbrella.core.model.Time;
import java.util.Collection;
import java.util.List;
public interface TimeDb {
Collection<Time> listTimes(Collection<Long> taskIds) throws UmbrellaException;

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

@ -4,9 +4,10 @@ package de.srsoftware.umbrella.time; @@ -4,9 +4,10 @@ package de.srsoftware.umbrella.time;
import static de.srsoftware.umbrella.core.ConnectionProvider.connect;
import static de.srsoftware.umbrella.core.Constants.*;
import static de.srsoftware.umbrella.core.Paths.LIST;
import static de.srsoftware.umbrella.core.exceptions.UmbrellaException.forbidden;
import static de.srsoftware.umbrella.core.exceptions.UmbrellaException.missingFieldException;
import static de.srsoftware.umbrella.time.Constants.*;
import static java.util.stream.Collectors.toMap;
import static java.util.function.Predicate.not;
import com.sun.net.httpserver.HttpExchange;
import de.srsoftware.configuration.Configuration;
@ -16,7 +17,6 @@ import de.srsoftware.umbrella.core.BaseHandler; @@ -16,7 +17,6 @@ import de.srsoftware.umbrella.core.BaseHandler;
import de.srsoftware.umbrella.core.api.*;
import de.srsoftware.umbrella.core.exceptions.UmbrellaException;
import de.srsoftware.umbrella.core.model.*;
import java.io.IOException;
import java.time.LocalDateTime;
import java.util.*;
@ -66,15 +66,68 @@ public class TimeModule extends BaseHandler implements TimeService { @@ -66,15 +66,68 @@ public class TimeModule extends BaseHandler implements TimeService {
}
}
/*
{
1 : {
name: Projekt 1
id: 1
times: {
3:{
name: time 3
start: 123456
end: 78901
},
4:{
name: time 4
start: 234567
end: 890123
tasks:{
5:{
name: task5
},
6:{
name: task6
}
}
}
}
},
2: {
name: Projekt 2
id: 2
times: {
7:{
name: time 7
start: 456789
end: 012345
}
}
}
}
*/
private boolean listTimes(HttpExchange ex, UmbrellaUser user) throws IOException, UmbrellaException {
var json = json(ex);
if (!(json.has(COMPANY_ID) && json.get(COMPANY_ID) instanceof Number cid)) throw missingFieldException(COMPANY_ID);
long companyId = cid.longValue();
if (!companies.membership(companyId,user.id())) throw UmbrellaException.forbidden("You are not a member of compayn {0}",companyId);
var projectMap = projects.listProjects(companyId,false).stream().collect(toMap(Project::id, p -> p));
var taskMap = tasks.listCompanyTasks(companyId).stream().collect(Collectors.toMap(Task::id,t->t));
var taskIds = taskMap.keySet();
var timesList = timeDb.listTimes(taskIds).stream().map();
return sendContent(ex,tree);
var companyId = cid.longValue();
var company = companies.get(companyId);
if (!companies.membership(companyId,user.id())) throw forbidden("You are mot a member of company {0}",company.name());
if (!(json.has(PROJECT_ID) && json.get(PROJECT_ID) instanceof Number pid)) throw missingFieldException(PROJECT_ID);
long projectId = pid.longValue();
Map<Long,Task> tasksOfProject = tasks.listProjectTasks(projectId).stream().collect(Collectors.toMap(Task::id,t->t));
List<Map<String, Object>> times = timeDb.listTimes(tasksOfProject.keySet())
.stream().filter(not(Time::isClosed))
.sorted(Comparator.comparing(Time::start))
.map(time -> {
var map = time.toMap();
var timeTasks = time.taskIds().stream().map(tasksOfProject::get).map(Task::toMap).toList();
map.put(TASKS,timeTasks);
return map;
}).toList();
return sendContent(ex,times);
}
}

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

@ -3,6 +3,7 @@ @@ -3,6 +3,7 @@
"loading": "lade…"
},
"document": {
"abort": "abbrechen",
"actions": "Aktionen",
"add_new": "{0} anlegen",
"add_position": "hinzufügen",
@ -17,9 +18,12 @@ @@ -17,9 +18,12 @@
"date": "Datum",
"delete": "löschen",
"email": "E-Mail",
"estimated_time": "geschätzte Zeit",
"estimated_times": "geschätzte Zeiten",
"footer": "Fuß-Text",
"gross_sum": "Brutto-Summe",
"head": "Kopf-Text",
"items": "Artikel",
"list": "Dokumente",
"list_of": "Dokumente von {0}",
"net_price": "Nettopreis",
@ -42,6 +46,7 @@ @@ -42,6 +46,7 @@
"state_sent": "versendet",
"tax_id": "Steuernummer",
"tax_rate": "Steuersatz",
"timetrack": "Zeiterfassung",
"title_or_desc": "Titel/Beschreibung",
"type": "Dokumententyp",
"type_confirmation": "Bestätigung",

Loading…
Cancel
Save