preparing for adding positions to document
This commit is contained in:
@@ -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";
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
}
|
||||
|
||||
@@ -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{
|
||||
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{
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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 {
|
||||
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 {
|
||||
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);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,20 +6,43 @@
|
||||
|
||||
let { close = () => {}, doc = $bindable({}), onSelect = (item) => {} } = $props();
|
||||
|
||||
let select = $state(0);
|
||||
let source = $state(0);
|
||||
|
||||
function select(position){
|
||||
close();
|
||||
onSelect(position);
|
||||
}
|
||||
|
||||
function estimateSelected(estimate){
|
||||
onSelect({task:estimate});
|
||||
close();
|
||||
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 @@
|
||||
|
||||
<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} />
|
||||
|
||||
@@ -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 @@
|
||||
}
|
||||
}
|
||||
|
||||
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>
|
||||
@@ -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 @@
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
@@ -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 {
|
||||
}
|
||||
|
||||
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 {
|
||||
|
||||
@@ -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){
|
||||
|
||||
@@ -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 {
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
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 {
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
{
|
||||
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);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
"loading": "lade…"
|
||||
},
|
||||
"document": {
|
||||
"abort": "abbrechen",
|
||||
"actions": "Aktionen",
|
||||
"add_new": "{0} anlegen",
|
||||
"add_position": "hinzufügen",
|
||||
@@ -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 @@
|
||||
"state_sent": "versendet",
|
||||
"tax_id": "Steuernummer",
|
||||
"tax_rate": "Steuersatz",
|
||||
"timetrack": "Zeiterfassung",
|
||||
"title_or_desc": "Titel/Beschreibung",
|
||||
"type": "Dokumententyp",
|
||||
"type_confirmation": "Bestätigung",
|
||||
|
||||
Reference in New Issue
Block a user