Browse Source

implemented opening and closing of projects right from the project list

kanban
Stephan Richter 4 months ago
parent
commit
680afd7700
  1. 2
      core/src/main/java/de/srsoftware/umbrella/core/model/Project.java
  2. BIN
      frontend/public/fontawesome-webfont.woff
  3. 6
      frontend/src/Components/CompanySelector.svelte
  4. 3
      frontend/src/routes/project/Create.svelte
  5. 56
      frontend/src/routes/project/List.svelte
  6. 21
      project/src/main/java/de/srsoftware/umbrella/project/ProjectModule.java
  7. 3
      translations/src/main/resources/de.json
  8. 11
      web/src/main/resources/web/css/default.css
  9. BIN
      web/src/main/resources/web/fontawesome-webfont.woff

2
core/src/main/java/de/srsoftware/umbrella/core/model/Project.java

@ -72,7 +72,7 @@ public class Project implements Mappable {
switch (key){ switch (key){
case DESCRIPTION: description = json.getString(key); break; case DESCRIPTION: description = json.getString(key); break;
case NAME: name = json.getString(key); break; case NAME: name = json.getString(key); break;
case STATUS: status = Status.of(json.getInt(key)); break; case STATUS: status = json.get(key) instanceof Number number ? Status.of(number.intValue()) : Status.valueOf(json.getString(key)); break;
default: key = null; default: key = null;
} }
if (key != null) dirtyFields.add(key); if (key != null) dirtyFields.add(key);

BIN
frontend/public/fontawesome-webfont.woff

Binary file not shown.

6
frontend/src/Components/CompanySelector.svelte

@ -6,6 +6,8 @@
let companies = $state(null); let companies = $state(null);
let value = 0; let value = 0;
let sortedCompanies = $derived.by(() => Object.values(companies).sort((a, b) => a.name.localeCompare(b.name)));
async function loadCompanies(){ async function loadCompanies(){
const url = `${location.protocol}//${location.host.replace('5173','8080')}/api/company/list`; const url = `${location.protocol}//${location.host.replace('5173','8080')}/api/company/list`;
var resp = await fetch(url,{ credentials: 'include'}); var resp = await fetch(url,{ credentials: 'include'});
@ -27,8 +29,8 @@
{#if companies} {#if companies}
<select onchange={select} bind:value> <select onchange={select} bind:value>
<option value={0}>{caption}</option> <option value={0}>{caption}</option>
{#each companies as company,idx} {#each sortedCompanies as company}
<option value={idx}>{company.name}</option> <option value={company.id}>{company.name}</option>
{/each} {/each}
</select> </select>
{:else} {:else}

3
frontend/src/routes/project/Create.svelte

@ -26,7 +26,8 @@
body: JSON.stringify(project) body: JSON.stringify(project)
}); });
if (resp.ok){ if (resp.ok){
router.navigate('/project'); var newProject = await resp.json();
router.navigate(`/project/${newProject.id}/view`);
} else { } else {
error = await resp.text(); error = await resp.text();
} }

56
frontend/src/routes/project/List.svelte

@ -7,6 +7,7 @@
let error = $state(null); let error = $state(null);
let projects = $state(null); let projects = $state(null);
let companies = $state(null); let companies = $state(null);
let showClosed = $state(router.query.closed == "show");
let sortedProjects = $derived.by(() => Object.values(projects).sort((a, b) => a.name.localeCompare(b.name))); let sortedProjects = $derived.by(() => Object.values(projects).sort((a, b) => a.name.localeCompare(b.name)));
@ -16,7 +17,11 @@
if (resp.ok){ if (resp.ok){
companies = await resp.json(); companies = await resp.json();
url = `${location.protocol}//${location.host.replace('5173','8080')}/api/project/list`; url = `${location.protocol}//${location.host.replace('5173','8080')}/api/project/list`;
resp = await fetch(url,{credentials:'include'}); resp = await fetch(url,{
credentials:'include',
method:'POST',
body:JSON.stringify({show_closed:showClosed})
});
if (resp.ok){ if (resp.ok){
projects = await resp.json(); projects = await resp.json();
} else { } else {
@ -27,6 +32,31 @@
} }
} }
async function setState(pid,state_name){
const url = `${location.protocol}//${location.host.replace('5173','8080')}/api/project/${pid}`;
const resp = await fetch(url,{
credentials:'include',
method:'PATCH',
body:JSON.stringify({status:state_name})
});
if (resp.ok){
var prj = await resp.json();
projects[prj.id].status = prj.status;
} else {
error = await resp.text();
}
}
function show(pid){
router.navigate(`/project/${pid}/view`)
}
function toggleClosed(){
router.navigate(showClosed?'/project':'/project?closed=show');
showClosed = !showClosed;
loadProjects();
}
onMount(loadProjects); onMount(loadProjects);
</script> </script>
@ -36,10 +66,11 @@
<fieldset> <fieldset>
<legend> <legend>
{t('projects')} {t('projects')}
<button onclick={() => router.navigate('/project/add')}>{t('create_new')}</button> <button onclick={() => router.navigate('/project/add')}><span class="symbol"></span> {t('create_new_project')}</button>
<button onclick={toggleClosed}><span class="symbol"></span> {t(showClosed?'hide_closed':'show_closed')}</button>
</legend> </legend>
{#if projects} {#if projects}
<table> <table class="project list">
<thead> <thead>
<tr> <tr>
<th>{t('name')}</th> <th>{t('name')}</th>
@ -51,9 +82,11 @@
</thead> </thead>
<tbody> <tbody>
{#each sortedProjects as project} {#each sortedProjects as project}
<tr onclick={() => router.navigate(`/project/${project.id}/view`)}> <tr>
<td>{project.name}</td> <td class="name" onclick={() => show(project.id)} >
<td> {project.name}
</td>
<td class="company" onclick={() => show(project.id)} >
{#if project.company_id} {#if project.company_id}
{companies[project.company_id].name} {companies[project.company_id].name}
{/if} {/if}
@ -61,11 +94,20 @@
<td> <td>
{t("state_"+project.status.name.toLowerCase())} {t("state_"+project.status.name.toLowerCase())}
</td> </td>
<td> <td class="members" onclick={() => show(project.id)} >
{#each Object.entries(project.members) as [uid,member]} {#each Object.entries(project.members) as [uid,member]}
<div>{member.user.name}</div> <div>{member.user.name}</div>
{/each} {/each}
</td> </td>
<td class="actions">
<button class="edit symbol" title={t('edit')}></button>
{#if project.status.code < 60}
<button class="complete symbol" title={t('complete')} onclick={() => setState(project.id,'COMPLETE')} ></button>
<button class="abort symbol" title={t('abort')} onclick={() => setState(project.id,'CANCELLED')} ></button>
{:else}
<button class="open symbol" title={t('open')} onclick={() => setState(project.id,'OPEN')} ></button>
{/if}
</td>
</tr> </tr>
{/each} {/each}
</tbody> </tbody>

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

@ -9,7 +9,6 @@ import static de.srsoftware.umbrella.core.model.Permission.OWNER;
import static de.srsoftware.umbrella.core.model.Status.OPEN; import static de.srsoftware.umbrella.core.model.Status.OPEN;
import static de.srsoftware.umbrella.project.Constants.CONFIG_DATABASE; import static de.srsoftware.umbrella.project.Constants.CONFIG_DATABASE;
import static java.lang.Boolean.TRUE; import static java.lang.Boolean.TRUE;
import static java.net.HttpURLConnection.HTTP_NOT_IMPLEMENTED;
import static java.net.HttpURLConnection.HTTP_OK; import static java.net.HttpURLConnection.HTTP_OK;
import static java.util.Comparator.comparing; import static java.util.Comparator.comparing;
@ -54,7 +53,6 @@ public class ProjectModule extends BaseHandler implements ProjectService {
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 LIST -> listUserProjects(ex,user.get());
case null -> postProject(ex,user.get()); case null -> postProject(ex,user.get());
default -> { default -> {
var projectId = Long.parseLong(head); var projectId = Long.parseLong(head);
@ -108,7 +106,7 @@ public class ProjectModule extends BaseHandler implements ProjectService {
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 LIST -> listCompanyProjects(ex,user.get()); case LIST -> postProjectList(ex,user.get());
case null -> postProject(ex,user.get()); case null -> postProject(ex,user.get());
default -> super.doGet(path,ex); default -> super.doGet(path,ex);
}; };
@ -132,15 +130,20 @@ public class ProjectModule extends BaseHandler implements ProjectService {
return sendContent(ex,map); return sendContent(ex,map);
} }
private boolean postProjectList(HttpExchange ex, UmbrellaUser user) throws IOException {
var json = json(ex);
var showClosed = json.has(SHOW_CLOSED) && json.get(SHOW_CLOSED) instanceof Boolean bool ? bool : false;
if (json.has(COMPANY_ID) && json.get(COMPANY_ID) instanceof Number companyId) return listCompanyProjects(ex, user, companyId.longValue());
return listUserProjects(ex,user,showClosed);
}
public Collection<Project> listCompanyProjects(long companyId, boolean includeClosed) throws UmbrellaException { public Collection<Project> listCompanyProjects(long companyId, boolean includeClosed) throws UmbrellaException {
return projects.ofCompany(companyId, includeClosed).values().stream().sorted(comparing(Project::name)).toList(); return projects.ofCompany(companyId, includeClosed).values().stream().sorted(comparing(Project::name)).toList();
} }
private boolean listCompanyProjects(HttpExchange ex, UmbrellaUser user) throws IOException, UmbrellaException { private boolean listCompanyProjects(HttpExchange ex, UmbrellaUser user, long companyId) throws IOException, UmbrellaException {
var json = json(ex);
if (!(json.has(COMPANY_ID) && json.get(COMPANY_ID) instanceof Number cid)) throw missingFieldException(COMPANY_ID);
var companyId = cid.longValue();
var company = companies.get(companyId); var company = companies.get(companyId);
if (!companies.membership(companyId,user.id())) throw forbidden("You are mot a member of company {0}",company.name()); if (!companies.membership(companyId,user.id())) throw forbidden("You are mot a member of company {0}",company.name());
var projects = listCompanyProjects(companyId,false) var projects = listCompanyProjects(companyId,false)
@ -155,9 +158,9 @@ public class ProjectModule extends BaseHandler implements ProjectService {
return projects.ofUser(userId, includeClosed); return projects.ofUser(userId, includeClosed);
} }
private boolean listUserProjects(HttpExchange ex, UmbrellaUser user) throws IOException, UmbrellaException { private boolean listUserProjects(HttpExchange ex, UmbrellaUser user, boolean showClosed) throws IOException, UmbrellaException {
var projects = new HashMap<Long,Map<String,Object>>(); var projects = new HashMap<Long,Map<String,Object>>();
for (var entry : listUserProjects(user.id(),false).entrySet()) { for (var entry : listUserProjects(user.id(),showClosed).entrySet()) {
var project = entry.getValue(); var project = entry.getValue();
var map = project.toMap(); var map = project.toMap();
var members = new HashMap<Long,Map<String,Object>>(); var members = new HashMap<Long,Map<String,Object>>();

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

@ -22,6 +22,7 @@
"company": "Firma", "company": "Firma",
"company_optional": "Firma (optional)", "company_optional": "Firma (optional)",
"confirmation": "Bestätigung", "confirmation": "Bestätigung",
"complete": "abschließen",
"contact": "Kontakte", "contact": "Kontakte",
"contained_tax": "enthaltene Steuer", "contained_tax": "enthaltene Steuer",
"content": "Inhalt", "content": "Inhalt",
@ -166,6 +167,8 @@
"service": "Service", "service": "Service",
"settings" : "Eisntellungen", "settings" : "Eisntellungen",
"state": "Status", "state": "Status",
"state_cancelled": "abgebrochen",
"state_complete": "abgeschlossen",
"state_declined": "abgelehnt", "state_declined": "abgelehnt",
"state_delayed": "verspätet", "state_delayed": "verspätet",
"state_error": "Fehler", "state_error": "Fehler",

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

@ -1,3 +1,8 @@
@font-face {
font-family: "awesome";
src: url("../fontawesome-webfont.woff");
}
a { a {
color: orange; color: orange;
} }
@ -88,4 +93,10 @@ td, tr{
} }
.task.complete > .name:before { .task.complete > .name:before {
content: "✓ "; content: "✓ ";
}
.symbol {
font-family: awesome;
font-size: 20px;
font-weight: normal;
} }

BIN
web/src/main/resources/web/fontawesome-webfont.woff

Binary file not shown.
Loading…
Cancel
Save