refactored translations, preparing sending of document

This commit is contained in:
2025-07-17 13:03:49 +02:00
parent 7616aa9581
commit dc8de9707a
38 changed files with 430 additions and 390 deletions

View File

@@ -45,7 +45,7 @@ subprojects {
implementation("de.srsoftware:tools.http:6.0.4") implementation("de.srsoftware:tools.http:6.0.4")
implementation("de.srsoftware:tools.logging:1.3.2") implementation("de.srsoftware:tools.logging:1.3.2")
implementation("de.srsoftware:tools.optionals:1.0.0") implementation("de.srsoftware:tools.optionals:1.0.0")
implementation("de.srsoftware:tools.util:2.0.3") implementation("de.srsoftware:tools.util:2.0.4")
implementation("org.json:json:20240303") implementation("org.json:json:20240303")
} }

View File

@@ -27,6 +27,7 @@ public abstract class BaseHandler extends PathHandler {
headers.add("Access-Control-Allow-Headers", "Content-Type"); headers.add("Access-Control-Allow-Headers", "Content-Type");
headers.add("Access-Control-Allow-Credentials", "true"); headers.add("Access-Control-Allow-Credentials", "true");
headers.add("Access-Control-Allow-Methods","DELETE, GET, POST, PATCH"); headers.add("Access-Control-Allow-Methods","DELETE, GET, POST, PATCH");
headers.add("Access-Control-Expose-Headers","Content-Disposition");
} }
return ex; return ex;
} }

View File

@@ -1,17 +0,0 @@
/* © SRSoftware 2025 */
package de.srsoftware.umbrella.core;
public class Tuple<A,B>{
public final A a;
public final B b;
public Tuple(A a, B b){
this.a = a;
this.b = b;
}
public static <A, B> Tuple<A,B> of(A a, B b) {
return new Tuple<>(a,b);
}
}

View File

@@ -37,8 +37,8 @@ import de.srsoftware.document.zugferd.data.Currency;
import de.srsoftware.tools.Pair; import de.srsoftware.tools.Pair;
import de.srsoftware.tools.Path; import de.srsoftware.tools.Path;
import de.srsoftware.tools.SessionToken; import de.srsoftware.tools.SessionToken;
import de.srsoftware.tools.Tuple;
import de.srsoftware.umbrella.core.BaseHandler; import de.srsoftware.umbrella.core.BaseHandler;
import de.srsoftware.umbrella.core.Tuple;
import de.srsoftware.umbrella.core.api.CompanyService; import de.srsoftware.umbrella.core.api.CompanyService;
import de.srsoftware.umbrella.core.api.Translator; import de.srsoftware.umbrella.core.api.Translator;
import de.srsoftware.umbrella.core.api.UserService; import de.srsoftware.umbrella.core.api.UserService;
@@ -199,20 +199,25 @@ public class DocumentApi extends BaseHandler {
case TEMPLATES -> postTemplateList(ex,user.get()); case TEMPLATES -> postTemplateList(ex,user.get());
case null -> postDocument(ex,user.get()); case null -> postDocument(ex,user.get());
default -> { default -> {
var docId = 0L; var docId = Long.parseLong(head);
try { yield switch (path.pop()){
docId = Long.parseLong(head); case null -> postToDocument(ex,path,user.get(),docId);
} catch (NumberFormatException ignored) { case PATH_SEND -> sendDocument(ex,path,user.get(),docId);
yield super.doPost(path,ex); default -> super.doPost(path,ex);
} };
yield postToDocument(ex,path,user.get(),docId);
} }
}; };
} catch (NumberFormatException ignored) {
return super.doPost(path,ex);
} catch (UmbrellaException e) { } catch (UmbrellaException e) {
return send(ex,e); return send(ex,e);
} }
} }
private boolean sendDocument(HttpExchange ex, Path path, UmbrellaUser umbrellaUser, long docId) throws IOException {
return sendEmptyResponse(HTTP_NOT_IMPLEMENTED,ex);
}
private boolean getCompanies(HttpExchange ex, UmbrellaUser user, Token token) throws IOException, UmbrellaException { private boolean getCompanies(HttpExchange ex, UmbrellaUser user, Token token) throws IOException, UmbrellaException {
return sendContent(ex,companies.listCompaniesOf(user).stream().map(Company::toMap)); return sendContent(ex,companies.listCompaniesOf(user).stream().map(Company::toMap));
} }
@@ -350,7 +355,7 @@ public class DocumentApi extends BaseHandler {
.filter(filter) .filter(filter)
.findAny(); .findAny();
if (optDoc.isEmpty()) throw UmbrellaException.notFound("Cannot render {0} {1}: Missing template \"{2}\"",type,document.number(),template); if (optDoc.isEmpty()) throw UmbrellaException.notFound("Cannot render {0} {1}: Missing template \"{2}\"",type,document.number(),template);
Function<String,String> translate = text -> translator.translate(user.language(),text); Function<String,String> translate = text -> translator.translate(user.language(),"document."+text.replaceAll(" ","_"));
var pdfData = new HashMap<String,Object>(); var pdfData = new HashMap<String,Object>();
pdfData.put(FIELD_DOCUMENT,document.renderToMap()); pdfData.put(FIELD_DOCUMENT,document.renderToMap());
pdfData.put("translate",translate); pdfData.put("translate",translate);

View File

@@ -13,6 +13,7 @@ import de.srsoftware.configuration.JsonConfig;
import de.srsoftware.document.api.*; import de.srsoftware.document.api.*;
import de.srsoftware.umbrella.core.model.UmbrellaUser; import de.srsoftware.umbrella.core.model.UmbrellaUser;
import java.util.*; import java.util.*;
import java.util.function.Function;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import org.json.JSONObject; import org.json.JSONObject;
@@ -77,8 +78,11 @@ public abstract class TemplateDoc implements Document {
} }
} }
tokens = findTokensIn(source); tokens = findTokensIn(source);
var opt = data.get("translate");
@SuppressWarnings("unchecked")
Function<String,String> translate = opt instanceof Function<?,?> fun ? (Function<String,String>) fun : s -> s;
for (var token : tokens){ for (var token : tokens){
var value = config.get("translations."+token).map(Object::toString).orElse(token).trim(); var value = translate.apply(token);
if (MIME_HTML.equals(mimeType())) value = value.replace("\n","<span class=\"break\"></span>\n"); if (MIME_HTML.equals(mimeType())) value = value.replace("\n","<span class=\"break\"></span>\n");
source = source.replace("<? "+token+" ?>",value); source = source.replace("<? "+token+" ?>",value);
} }

View File

@@ -132,25 +132,25 @@
<div class="sender data"> <div class="sender data">
<? document.sender.name ?><br/> <? document.sender.name ?><br/>
<br/> <br/>
<? Delivery date ?>: <? document.delivery ?> <? delivery date ?>: <? document.delivery ?>
</div> </div>
</header> </header>
<h1><? document.type ?></h1> <h1><? document.type ?></h1>
<h2> <h2>
<span class="left"><? document.type ?> <? document.number ?></span> <span class="left"><? document.type ?> <? document.number ?></span>
<span class="center"><? customer number ?>: <? document.customer.id ?></span> <span class="center"><? customer id ?>: <? document.customer.id ?></span>
<span class="right"><? document.date ?></span> <span class="right"><? document.date ?></span>
</h2> </h2>
<hr/> <hr/>
<div class="head"><? document.head.rendered ?></div> <div class="head"><? document.head.rendered ?></div>
<table> <table>
<tr> <tr>
<th><? Position ?></th> <th><? position ?></th>
<th><? Description ?></th> <th><? description ?></th>
<th><? Price per unit ?></th> <th><? price per unit ?></th>
<th><? Amount ?></th> <th><? amount ?></th>
<th><? Price ?></th> <th><? price ?></th>
<th><? Tax rate ?></th> <th><? tax rate ?></th>
</tr> </tr>
<!-- positions --> <!-- positions -->
@@ -171,7 +171,7 @@
<!-- positions --> <!-- positions -->
<tr> <tr>
<th class="right" colspan="5"><? Net sum ?>:</th> <th class="right" colspan="5"><? net sum ?>:</th>
<th class="right"><? document.net_sum ?></th> <th class="right"><? document.net_sum ?></th>
</tr> </tr>
@@ -181,13 +181,13 @@
<td></td> <td></td>
<td><? net_sum ?>&nbsp;+&nbsp;<? rate ?>&nbsp;=</td> <td><? net_sum ?>&nbsp;+&nbsp;<? rate ?>&nbsp;=</td>
<td><? gross_sum ?></td> <td><? gross_sum ?></td>
<td colspan="2"><? contained tax: ?></td> <td colspan="2"><? contained tax ?>:</td>
<td><? amount ?></td> <td><? amount ?></td>
</tr> </tr>
<!-- tax list --> <!-- tax list -->
<tr> <tr>
<th class="right" colspan="5"><? Gross sum ?>:</th> <th class="right" colspan="5"><? gross sum ?>:</th>
<th class="right"><? document.gross_sum ?></th> <th class="right"><? document.gross_sum ?></th>
</tr> </tr>
</table> </table>
@@ -196,12 +196,12 @@
<tr> <tr>
<td><? document.sender.name ?></td> <td><? document.sender.name ?></td>
<td> <td>
<? Tax id ?>: <? document.sender.tax_id ?><br/> <? tax id ?>: <? document.sender.tax_id ?><br/>
<? local court ?>: <? document.sender.court ?><br/><br/> <? local court ?>: <? document.sender.court ?><br/><br/>
<div class="ad"><? document.type ?> <? created with ?> <a href="https://umbrella.srsoftware.de">Umbrella</a> <? by ?> <a href="https://srsoftware.de">SRSoftware</a></div> <div class="ad"><? document.type ?> <? created with ?> <a href="https://umbrella.srsoftware.de">Umbrella</a> <? by ?> <a href="https://srsoftware.de">SRSoftware</a></div>
</td> </td>
<td> <td>
<? Bank account ?>:<br/> <? bank account ?>:<br/>
<? document.sender.bank_account ?> <? document.sender.bank_account ?>
</td> </td>
</tr> </tr>
@@ -216,7 +216,7 @@
<div class="ad"><? document.type ?> <? created with ?> <a href="https://umbrella.srsoftware.de">Umbrella</a> <? by ?> <a href="https://srsoftware.de">SRSoftware</a></div> <div class="ad"><? document.type ?> <? created with ?> <a href="https://umbrella.srsoftware.de">Umbrella</a> <? by ?> <a href="https://srsoftware.de">SRSoftware</a></div>
</div> </div>
<div class="bank_account"> <div class="bank_account">
<? Bank account ?>:<br/> <? bank account ?>:<br/>
<? document.sender.bank_account ?> <? document.sender.bank_account ?>
</div> </div>
</div> --> </div> -->

View File

@@ -14,6 +14,7 @@
import Menu from "./Components/Menu.svelte"; import Menu from "./Components/Menu.svelte";
import ResetPw from "./routes/user/ResetPw.svelte"; import ResetPw from "./routes/user/ResetPw.svelte";
import Search from "./routes/search/Search.svelte"; import Search from "./routes/search/Search.svelte";
import SendDoc from "./routes/document/Send.svelte";
import User from "./routes/user/User.svelte"; import User from "./routes/user/User.svelte";
import ViewDoc from "./routes/document/View.svelte"; import ViewDoc from "./routes/document/View.svelte";
@@ -40,6 +41,7 @@
<Route path="/" component={User} /> <Route path="/" component={User} />
<Route path="/document" component={DocList} /> <Route path="/document" component={DocList} />
<Route path="/document/add" component={AddDoc} /> <Route path="/document/add" component={AddDoc} />
<Route path="/document/:id/send" component={SendDoc} />
<Route path="/document/:id/view" component={ViewDoc} /> <Route path="/document/:id/view" component={ViewDoc} />
<Route path="/message/settings" component={Messages} /> <Route path="/message/settings" component={Messages} />
<Route path="/search" component={Search} /> <Route path="/search" component={Search} />

View File

@@ -2,7 +2,7 @@
import {onMount} from 'svelte'; import {onMount} from 'svelte';
import {t} from '../translations.svelte.js'; import {t} from '../translations.svelte.js';
let { caption, onselect = (contact) => console.log('selected '+contact.FN||contact.ORG) } = $props(); let { caption, onselect = (contact) => console.log('selected '+contact.FN||contact.ORG) } = $props();
let message = t('contacts.loading'); let message = t('loading');
let contacts = $state(null); let contacts = $state(null);
let value = 0; let value = 0;

View File

@@ -8,7 +8,7 @@
<div title={'task_'+task.id}> <div title={'task_'+task.id}>
{#if task.estimated_time} {#if task.estimated_time}
<span class="estimate" onclick={() => onSelect(task)}> <span class="estimate" onclick={() => onSelect(task)}>
{task.estimated_time}&nbsp;{t(task.estimated_time != 1 ? 'task.hours' : 'task.hour')} {task.estimated_time}&nbsp;{t(task.estimated_time != 1 ? 'hours' : 'hour')}
</span> </span>
{/if} {/if}
{task.name} {task.name}

View File

@@ -3,5 +3,5 @@
</script> </script>
<footer> <footer>
{@html t('footer.message','<a href="https://srsoftware.de">SRSoftware</a>')} {@html t('advertisement','<a href="https://srsoftware.de">SRSoftware</a>')}
</footer> </footer>

View File

@@ -1,6 +1,4 @@
<script> <script>
import { t } from '../translations.svelte.js';
let { item, onclick } = $props(); let { item, onclick } = $props();
</script> </script>

View File

@@ -65,21 +65,21 @@
<form onsubmit={doLogin}> <form onsubmit={doLogin}>
<fieldset> <fieldset>
<legend>{t('login.Login')}</legend> <legend>{t('login')}</legend>
<label> <label>
<input type="text" bind:value={credentials.username} required use:init /> <input type="text" bind:value={credentials.username} required use:init />
<span>{t('login.Email_or_Username')}</span> <span>{t('email_or_username')}</span>
</label> </label>
<label> <label>
<input type="password" bind:value={credentials.password} required /> <input type="password" bind:value={credentials.password} required />
<span>{t('login.Password')}</span> <span>{t('password')}</span>
</label> </label>
<button>{t('login.do_login')}</button> <button>{t('do_login')}</button>
<a onclick={resetPW}>{t('login.forgot_pass')}</a> <a onclick={resetPW}>{t('forgot_pass')}</a>
</fieldset> </fieldset>
</form> </form>
<fieldset> <fieldset>
<legend>{t('login.OIDC_Login')}</legend> <legend>{t('oidc_Login')}</legend>
{#each services as service,i} {#each services as service,i}
<button onclick={() => redirectTo(service)}>{service}</button> <button onclick={() => redirectTo(service)}>{service}</button>
{/each} {/each}

View File

@@ -13,7 +13,7 @@ async function fetchModules(){
const resp = await fetch(url,{credentials:'include'}); const resp = await fetch(url,{credentials:'include'});
if (resp.ok){ if (resp.ok){
const arr = await resp.json(); const arr = await resp.json();
for (let entry of arr) modules.push({name:t('menu.'+entry.module),url:entry.url}); for (let entry of arr) modules.push({name:t(entry.module),url:entry.url});
} else { } else {
console.log('error'); console.log('error');
} }
@@ -28,13 +28,13 @@ onMount(fetchModules);
</style> </style>
<nav> <nav>
<a onclick={() => router.navigate('/user')}>{t('menu.users')}</a> <a onclick={() => router.navigate('/user')}>{t('users')}</a>
<a onclick={() => router.navigate('/document')}>{t('menu.documents')}</a> <a onclick={() => router.navigate('/document')}>{t('documents')}</a>
<a href="https://svelte.dev/tutorial/svelte/state" target="_blank">{t('menu.tutorial')}</a> <a href="https://svelte.dev/tutorial/svelte/state" target="_blank">{t('tutorial')}</a>
{#each modules as module,i} {#each modules as module,i}
<a href={module.url}>{module.name}</a> <a href={module.url}>{module.name}</a>
{/each} {/each}
{#if user.name } {#if user.name }
<a onclick={logout}>{t('menu.logout')}</a> <a onclick={logout}>{t('logout')}</a>
{/if} {/if}
</nav> </nav>

View File

@@ -43,7 +43,7 @@
var resp = await fetch(url,{ credentials: 'include'}); var resp = await fetch(url,{ credentials: 'include'});
if (resp.ok){ if (resp.ok){
const types = await resp.json(); const types = await resp.json();
docType = t('document.type_'+types[document.type]); docType = t('type_'+types[document.type]);
} else { } else {
error = await resp.text(); error = await resp.text();
} }
@@ -97,49 +97,49 @@
<span class="error">{error}</span> <span class="error">{error}</span>
{/if} {/if}
{#if docType} {#if docType}
<legend>{t('document.add_new',docType)}</legend> <legend>{t('add_new',docType)}</legend>
{/if} {/if}
{#if company} {#if company}
Company: {company.name} Company: {company.name}
<fieldset> <fieldset>
<legend>{t('document.customer')}</legend> <legend>{t('customer')}</legend>
<ContactSelector caption={t('document.select_customer')} onselect={contactSelected} /> <ContactSelector caption={t('select_customer')} onselect={contactSelected} />
<label> <label>
<textarea bind:value={document.customer.name}></textarea> <textarea bind:value={document.customer.name}></textarea>
{t('document.customer_address')} {t('customer_address')}
</label> </label>
<label> <label>
<input bind:value={document.customer.tax_id} /> <input bind:value={document.customer.tax_id} />
{t('document.tax_id')} {t('tax_id')}
</label> </label>
<label> <label>
<input bind:value={document.customer.id} /> <input bind:value={document.customer.id} />
{t('document.customer_id')} {t('customer_id')}
</label> </label>
<label> <label>
<input bind:value={document.customer.email} /> <input bind:value={document.customer.email} />
{t('document.email')} {t('email')}
</label> </label>
</fieldset> </fieldset>
{/if} {/if}
<fieldset> <fieldset>
<legend>{t('document.sender')}</legend> <legend>{t('sender')}</legend>
<label> <label>
<textarea bind:value={document.sender.name}></textarea> <textarea bind:value={document.sender.name}></textarea>
{t('document.sender_name')} {t('sender_name')}
</label> </label>
<label> <label>
<input bind:value={document.sender.tax_id} /> <input bind:value={document.sender.tax_id} />
{t('document.sender_tax_id')} {t('sender_tax_id')}
</label> </label>
<label> <label>
<textarea bind:value={document.sender.bank_account}></textarea> <textarea bind:value={document.sender.bank_account}></textarea>
{t('document.sender_bank_account')} {t('sender_bank_account')}
</label> </label>
<label> <label>
<input bind:value={document.sender.court} /> <input bind:value={document.sender.court} />
{t('document.sender_local_court')} {t('sender_local_court')}
</label> </label>
</fieldset> </fieldset>
<button onclick={submit}>{t('document.create_new')}</button> <button onclick={submit}>{t('create_new')}</button>
</fieldset> </fieldset>

View File

@@ -35,7 +35,7 @@
</style> </style>
<div> <div>
<h1>{t('task.estimated_times')}</h1> <h1>{t('estimated_times')}</h1>
{#if error} {#if error}
<span class="error">{error}</span> <span class="error">{error}</span>
{/if} {/if}

View File

@@ -28,7 +28,7 @@
</script> </script>
<div> <div>
<h1>{t('items.items')}</h1> <h1>{t('items')}</h1>
{#if error} {#if error}
<span class="error">{error}</span> <span class="error">{error}</span>
{/if} {/if}

View File

@@ -45,7 +45,7 @@
} }
async function deleteDoc(ev,doc){ async function deleteDoc(ev,doc){
if (confirm(t('document.really_delete',doc.number))){ if (confirm(t('really_delete',doc.number))){
const url = `${location.protocol}//${location.host.replace('5173','8080')}/api/document/${doc.id}`; const url = `${location.protocol}//${location.host.replace('5173','8080')}/api/document/${doc.id}`;
const resp = await fetch(url,{ const resp = await fetch(url,{
credentials: 'include', credentials: 'include',
@@ -63,12 +63,12 @@
</script> </script>
<fieldset> <fieldset>
<legend>{selected_company ? t( 'document.list_of',selected_company.name) : t('document.list')}</legend> <legend>{selected_company ? t( 'list_of',selected_company.name) : t('document.list')}</legend>
{#if error} {#if error}
<div class="error">{error}</div> <div class="error">{error}</div>
{/if} {/if}
<div> <div>
{t('document.select_company')} {t('select_company')}
{#each Object.entries(companies) as [id,company]} {#each Object.entries(companies) as [id,company]}
<button onclick={() => load(company)}>{company.name}</button> <button onclick={() => load(company)}>{company.name}</button>
{/each} {/each}
@@ -77,15 +77,15 @@
<table> <table>
<thead> <thead>
<tr> <tr>
<th>{t('document.number')}</th> <th>{t('number')}</th>
<th>{t('document.date')}</th> <th>{t('date')}</th>
<th>{t('document.customer')}</th> <th>{t('customer')}</th>
<th>{t('document.gross_sum')}</th> <th>{t('gross_sum')}</th>
<th>{t('document.type')}</th> <th>{t('type')}</th>
<th>{t('document.state')}</th> <th>{t('state')}</th>
<th> <th>
{t('document.actions')} {t('actions')}
<TypeSelector caption={t('document.create_new')} bind:value={docType} onchange={createDoc} /> <TypeSelector caption={t('create_new_document')} bind:value={docType} onchange={createDoc} />
</th> </th>
</tr> </tr>
</thead> </thead>
@@ -96,11 +96,11 @@
<td onclick={() => show(id)}>{document.date}</td> <td onclick={() => show(id)}>{document.date}</td>
<td onclick={() => show(id)}>{document.customer.name.split('\n')[0]}</td> <td onclick={() => show(id)}>{document.customer.name.split('\n')[0]}</td>
<td onclick={() => show(id)}>{document.sum/100 + document.currency}</td> <td onclick={() => show(id)}>{document.sum/100 + document.currency}</td>
<td onclick={() => show(id)}>{t('document.type_'+document.type)}</td> <td onclick={() => show(id)}>{t('type_'+document.type)}</td>
<td onclick={() => show(id)}>{t('document.state_'+document.state.name)}</td> <td onclick={() => show(id)}>{t('state_'+document.state.name)}</td>
<td> <td>
{#if document.state.id == 1} {#if document.state.id == 1}
<button onclick={(ev) => deleteDoc(ev,document)}>{t('document.delete')}</button> <button onclick={(ev) => deleteDoc(ev,document)}>{t('delete')}</button>
{/if} {/if}
</td> </td>
</tr> </tr>

View File

@@ -1,7 +1,6 @@
<script> <script>
import { useTinyRouter } from 'svelte-tiny-router'; import { useTinyRouter } from 'svelte-tiny-router';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import { t } from '../../translations.svelte.js';
import LineEditor from '../../Components/LineEditor.svelte'; import LineEditor from '../../Components/LineEditor.svelte';
import MarkdownEditor from '../../Components/MarkdownEditor.svelte'; import MarkdownEditor from '../../Components/MarkdownEditor.svelte';
import PriceEditor from '../../Components/PriceEditor.svelte'; import PriceEditor from '../../Components/PriceEditor.svelte';

View File

@@ -30,7 +30,7 @@
} }
async function drop(number){ async function drop(number){
let confirmed = confirm(t('document.confirm_deletion').replace('{pos}',document.positions[number].item)); let confirmed = confirm(t('confirm_deletion').replace('{pos}',document.positions[number].item));
if (!confirmed) return; if (!confirmed) return;
const url = `${location.protocol}//${location.host.replace('5173','8080')}/api/document/${document.id}/position`; const url = `${location.protocol}//${location.host.replace('5173','8080')}/api/document/${document.id}/position`;
const resp = await fetch(url,{ const resp = await fetch(url,{
@@ -50,14 +50,14 @@
<table class="positions"> <table class="positions">
<thead> <thead>
<tr> <tr>
<th>{t('document.pos')}</th> <th>{t('pos')}</th>
<th>{t('document.code')}</th> <th>{t('code')}</th>
<th>{t('document.title_or_desc')}</th> <th>{t('title_or_desc')}</th>
<th>{t('document.amount')}</th> <th>{t('amount')}</th>
<th>{t('document.unit')}</th> <th>{t('unit')}</th>
<th>{t('document.unit_price')}</th> <th>{t('unit_price')}</th>
<th>{t('document.net_price')}</th> <th>{t('net_price')}</th>
<th>{t('document.tax_rate')}</th> <th>{t('tax_rate')}</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@@ -66,9 +66,9 @@
{/each} {/each}
<tr class="sums"> <tr class="sums">
<td colspan="2"></td> <td colspan="2"></td>
<td>{t('document.net_sum')}</td> <td>{t('net_sum')}</td>
<td>{document.net_sum/100}&nbsp;{document.currency}</td> <td>{document.net_sum/100}&nbsp;{document.currency}</td>
<td colspan="2">{t('document.gros_sum')}</td> <td colspan="2">{t('gros_sum')}</td>
<td>{document.gross_sum/100}&nbsp;{document.currency}</td> <td>{document.gross_sum/100}&nbsp;{document.currency}</td>
<td></td> <td></td>
</tr> </tr>

View File

@@ -19,7 +19,7 @@
title:estimate.name, title:estimate.name,
description:estimate.description.source, description:estimate.description.source,
amount:estimate.estimated_time, amount:estimate.estimated_time,
unit:t('document.hours') unit:t('hours')
}); });
} }
@@ -37,11 +37,11 @@
function timeSelected(time){ function timeSelected(time){
select({ select({
item_code:t('document.timetrack'), item_code:t('timetrack'),
title:time.subject, title:time.subject,
description:time.description.source, description:time.description.source,
amount:time.duration, amount:time.duration,
unit:t('document.hours'), unit:t('hours'),
time_id:time.id time_id:time.id
}); });
} }
@@ -66,10 +66,10 @@
<div class="position_selector"> <div class="position_selector">
<span class="tabs"> <span class="tabs">
<button onclick={() => source=0}>{t('document.items')}</button> <button onclick={() => source=0}>{t('items')}</button>
<button onclick={() => source=1}>{t('document.estimated_times')}</button> <button onclick={() => source=1}>{t('estimated_times')}</button>
<button onclick={() => source=2}>{t('document.timetrack')}</button> <button onclick={() => source=2}>{t('timetrack')}</button>
<button onclick={close}>{t('document.abort')}</button> <button onclick={close}>{t('abort')}</button>
</span> </span>
{#if source == 0} {#if source == 0}
<ItemList company_id={doc.company.id} onSelect={itemSelected} /> <ItemList company_id={doc.company.id} onSelect={itemSelected} />

View File

@@ -0,0 +1,24 @@
<script>
import { onMount } from 'svelte';
import { t } from '../../translations.svelte.js';
import { useTinyRouter } from 'svelte-tiny-router';
const router = useTinyRouter();
let { id } = $props();
let error = $state(null);
</script>
{#if error}
<span class="error">{error}</span>
{/if}
<fieldset>
<legend>{t('customer_email')}</legend>
</fieldset>
<fieldset>
<legend>{t('content')}</legend>
</fieldset>
<fieldset>
<legend>{t('actions')}</legend>
</fieldset>

View File

@@ -1,9 +1,9 @@
<script> <script>
import {onMount} from 'svelte'; import {onMount} from 'svelte';
import {t} from '../../translations.svelte.js'; import {t} from '../../translations.svelte.js';
let { caption = t('document.select_state'), selected = $bindable(0), onchange = (val) => console.log('changed to '+val)} = $props(); let { caption = t('select_state'), selected = $bindable(0), onchange = (val) => console.log('changed to '+val)} = $props();
let message = $state(t('document.loading')); let message = $state(t('loading'));
let states = $state(null); let states = $state(null);
async function loadStates(){ async function loadStates(){
@@ -22,7 +22,7 @@
{#if states} {#if states}
<select bind:value={selected} onchange={() => onchange(selected)}> <select bind:value={selected} onchange={() => onchange(selected)}>
{#each Object.entries(states) as [k,s]} {#each Object.entries(states) as [k,s]}
<option value={+k}>{t('document.state_'+s.toLowerCase())}</option> <option value={+k}>{t('state_'+s.toLowerCase())}</option>
{/each} {/each}
</select> </select>
{:else} {:else}

View File

@@ -2,7 +2,7 @@
import {onMount} from 'svelte'; import {onMount} from 'svelte';
import {t} from '../../translations.svelte.js'; import {t} from '../../translations.svelte.js';
let { caption, company, value = $bindable(0), onchange = () => console.log('changed')} = $props(); let { caption, company, value = $bindable(0), onchange = () => console.log('changed')} = $props();
let message = t('document.loading'); let message = t('loading');
let templates = $state(null); let templates = $state(null);
async function loadTemplates(){ async function loadTemplates(){

View File

@@ -22,7 +22,7 @@
<select bind:value onchange={onchange}> <select bind:value onchange={onchange}>
<option value={0}>{caption}</option> <option value={0}>{caption}</option>
{#each Object.entries(types) as [id,type]} {#each Object.entries(types) as [id,type]}
<option value={id}>{t('document.type_'+type)}</option> <option value={id}>{t('type_'+type)}</option>
{/each} {/each}
</select> </select>
{:else} {:else}

View File

@@ -9,9 +9,14 @@
import PositionSelector from './PositionSelector.svelte'; import PositionSelector from './PositionSelector.svelte';
import StateSelector from './StateSelector.svelte'; import StateSelector from './StateSelector.svelte';
import TemplateSelector from './TemplateSelector.svelte'; import TemplateSelector from './TemplateSelector.svelte';
const router = useTinyRouter();
let { id } = $props(); let { id } = $props();
let error = $state(null); let error = $state(null);
let doc = $state(null); let doc = $state(null);
let pdfDisabled = $state(false);
let sndDisabled = $state(false);
let position_select = $state(false); let position_select = $state(false);
let editable = $derived(doc.state == 1); let editable = $derived(doc.state == 1);
@@ -30,8 +35,8 @@
async function changeState(newVal){ async function changeState(newVal){
let success = false; let success = false;
if (doc.state == 1 || confirm(t('document.confirm_state'))){ if (doc.state == 1 || confirm(t('confirm_state'))){
success = await submit('state',newVal); success = await update('state',newVal);
} }
if (success) { if (success) {
doc.state = newVal; doc.state = newVal;
@@ -42,7 +47,7 @@
} }
} }
async function submit(path,newValue){ async function update(path,newValue){
const parts = path.split('.'); const parts = path.split('.');
if (parts.length<1) return false; if (parts.length<1) return false;
let data = newValue; let data = newValue;
@@ -79,9 +84,24 @@
} }
} }
async function render(){ async function render(ev){
pdfDisabled = true;
const url = `${location.protocol}//${location.host.replace('5173','8080')}/api/document/${doc.id}/pdf`; const url = `${location.protocol}//${location.host.replace('5173','8080')}/api/document/${doc.id}/pdf`;
location.href = url; const resp = await fetch(url,{credentials:'include'});
if (resp.ok){
error = null;
const blob = await resp.blob();
const parts = resp.headers.get("Content-disposition").split(";");
const filename = parts[1].split('=')[1].split('"').join('');
var fileLink = document.createElement('a');
fileLink.href = URL.createObjectURL(blob);
fileLink.download = filename;
fileLink.click();
} else {
error = await resp.text();
}
pdfDisabled = false;
} }
onMount(loadDoc); onMount(loadDoc);
@@ -93,90 +113,90 @@
{#if doc} {#if doc}
<fieldset class="customer"> <fieldset class="customer">
<legend>{t('document.customer')}</legend> <legend>{t('customer')}</legend>
<table> <table>
<tbody> <tbody>
<tr> <tr>
<td colspan="2"> <td colspan="2">
<MultilineEditor bind:value={doc.customer.name} editable={editable} onSet={(val) => submit('customer.name',val)} /> <MultilineEditor bind:value={doc.customer.name} editable={editable} onSet={(val) => update('customer.name',val)} />
</td> </td>
</tr> </tr>
<tr> <tr>
<th>{t('document.customer_id')}:</th> <th>{t('customer_id')}:</th>
<td> <td>
<LineEditor bind:value={doc.customer.id} editable={editable} onSet={(val) => submit('customer.id',val)} /> <LineEditor bind:value={doc.customer.id} editable={editable} onSet={(val) => update('customer.id',val)} />
</td> </td>
</tr> </tr>
<tr> <tr>
<th>{t('document.tax_id')}:</th> <th>{t('tax_id')}:</th>
<td> <td>
<LineEditor bind:value={doc.customer.tax_id} editable={editable} onSet={(val) => submit('customer.tax_id',val)} /> <LineEditor bind:value={doc.customer.tax_id} editable={editable} onSet={(val) => update('customer.tax_id',val)} />
</td> </td>
</tr> </tr>
<tr> <tr>
<th>{t('document.email')}:</th> <th>{t('email')}:</th>
<td> <td>
<LineEditor bind:value={doc.customer.email} editable={editable} onSet={(val) => submit('customer.email',val)} /> <LineEditor bind:value={doc.customer.email} editable={editable} onSet={(val) => update('customer.email',val)} />
</td> </td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
</fieldset> </fieldset>
<fieldset class="sender"> <fieldset class="sender">
<legend>{t('document.sender')}</legend> <legend>{t('sender')}</legend>
<table> <table>
<tbody> <tbody>
<tr> <tr>
<td colspan="2"> <td colspan="2">
<MultilineEditor bind:value={doc.sender.name} editable={editable} onSet={(val) => submit('sender.name',val)} /> <MultilineEditor bind:value={doc.sender.name} editable={editable} onSet={(val) => update('sender.name',val)} />
</td> </td>
</tr> </tr>
<tr> <tr>
<th>{t('document.court')}:</th> <th>{t('local_court')}:</th>
<td> <td>
<LineEditor bind:value={doc.sender.court} editable={editable} onSet={(val) => submit('sender.court',val)} /> <LineEditor bind:value={doc.sender.court} editable={editable} onSet={(val) => update('sender.court',val)} />
</td> </td>
</tr> </tr>
<tr> <tr>
<th>{t('document.tax_id')}:</th> <th>{t('tax_id')}:</th>
<td> <td>
<LineEditor bind:value={doc.sender.tax_id} editable={editable} onSet={(val) => submit('sender.tax_id',val)} /> <LineEditor bind:value={doc.sender.tax_id} editable={editable} onSet={(val) => update('sender.tax_id',val)} />
</td> </td>
</tr> </tr>
<tr> <tr>
<th>{t('document.bank_account')}:</th> <th>{t('bank_account')}:</th>
<td> <td>
<MultilineEditor bind:value={doc.sender.bank_account} editable={editable} onSet={(val) => submit('sender.bank_account',val)} /> <MultilineEditor bind:value={doc.sender.bank_account} editable={editable} onSet={(val) => update('sender.bank_account',val)} />
</td> </td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
</fieldset> </fieldset>
<fieldset class="invoice_data"> <fieldset class="invoice_data">
<legend>{t('document.type_'+doc.type)}</legend> <legend>{t('type_'+doc.type)}</legend>
<table> <table>
<tbody> <tbody>
<tr> <tr>
<th>{t('document.number')}:</th> <th>{t('number')}:</th>
<td><LineEditor bind:value={doc.number} editable={editable} onSet={(val) => submit('number',val)} /></td> <td><LineEditor bind:value={doc.number} editable={editable} onSet={(val) => update('number',val)} /></td>
</tr> </tr>
<tr> <tr>
<th>{t('document.state')}:</th> <th>{t('state')}:</th>
<StateSelector selected={doc.state} onchange={changeState} onSet={(val) => submit('state',val)} /> <StateSelector selected={doc.state} onchange={changeState} onSet={(val) => update('state',val)} />
</tr> </tr>
<tr> <tr>
<th>{t('document.date')}:</th> <th>{t('date')}:</th>
<LineEditor bind:value={doc.date} editable={editable} onSet={(val) => submit('date',val)} /> <LineEditor bind:value={doc.date} editable={editable} onSet={(val) => update('date',val)} />
</tr> </tr>
<tr> <tr>
<th>{t('document.delivery')}:</th> <th>{t('delivery_date')}:</th>
<LineEditor bind:value={doc.delivery} editable={editable} onSet={(val) => submit('delivery',val)} /> <LineEditor bind:value={doc.delivery} editable={editable} onSet={(val) => update('delivery',val)} />
</tr> </tr>
<tr> <tr>
<th>{t('document.template')}:</th> <th>{t('template')}:</th>
<td> <td>
{#if editable} {#if editable}
<TemplateSelector company={doc.company.id} bind:value={doc.template.id} onchange={() => submit('template_id',doc.template.id)} /> <TemplateSelector company={doc.company.id} bind:value={doc.template.id} onchange={() => update('template_id',doc.template.id)} />
{:else} {:else}
{doc.template.name} {doc.template.name}
{/if} {/if}
@@ -186,30 +206,30 @@
</table> </table>
</fieldset> </fieldset>
<fieldset class="clear"> <fieldset class="clear">
<legend>{t('document.head')}</legend> <legend>{t('head')}</legend>
<MarkdownEditor bind:value={doc.head} editable={editable} onSet={(val) => submit('head',val)} /> <MarkdownEditor bind:value={doc.head} editable={editable} onSet={(val) => update('head',val)} />
</fieldset> </fieldset>
<fieldset> <fieldset>
<legend> <legend>
{t('document.positions')} {t('positions')}
{#if editable} {#if editable}
<button onclick={() => position_select = true}>{t('document.add_position')}</button> <button onclick={() => position_select = true}>{t('add_position')}</button>
{/if} {/if}
</legend> </legend>
<PositionList bind:document={doc} {submit} bind:error={error} /> <PositionList bind:document={doc} {update} bind:error={error} />
</fieldset> </fieldset>
<fieldset> <fieldset>
<legend>{t('document.footer')}</legend> <legend>{t('footer')}</legend>
<MarkdownEditor bind:value={doc.footer} editable={editable} onSet={(val) => submit('footer',val)} /> <MarkdownEditor bind:value={doc.footer} editable={editable} onSet={(val) => update('footer',val)} />
</fieldset> </fieldset>
<fieldset> <fieldset>
<legend>{t('document.actions')}</legend> <legend>{t('actions')}</legend>
<button onclick={render}>{t('document.create_pdf')}</button> <button onclick={render} disabled={pdfDisabled}>{t('create_pdf')}</button>
<button onclick={() => router.navigate(`/document/${doc.id}/send`)} >{t('send_document')}</button>
</fieldset> </fieldset>
<fieldset> <fieldset>
<legend>TODO</legend> <legend>TODO</legend>
<ul> <ul>
<li>Button zum Rendern des Dokuments einbauen</li>
<li>Button zum Versenden des Dokuments einbauen</li> <li>Button zum Versenden des Dokuments einbauen</li>
<li>Preise in den Company-Einstellungen ändern, wenn für eine Position der Preis geändert wird siehe <em>documents.db</em>, Tabelle <em>customer_prices</em></li> <li>Preise in den Company-Einstellungen ändern, wenn für eine Position der Preis geändert wird siehe <em>documents.db</em>, Tabelle <em>customer_prices</em></li>
<li>Preise in der Tabelle neu berechnen, wenn Positionen geändert werden</li> <li>Preise in der Tabelle neu berechnen, wenn Positionen geändert werden</li>

View File

@@ -3,5 +3,5 @@
</script> </script>
<fieldset> <fieldset>
<legend>{t('message.messages')}</legend> <legend>{t('messages')}</legend>
</fieldset> </fieldset>

View File

@@ -16,7 +16,7 @@
}); });
if (resp.ok){ if (resp.ok){
html = await resp.text(); html = await resp.text();
if (!html) html = t('search.nothing_found'); if (!html) html = t('nothing_found');
} }
} }
@@ -29,23 +29,23 @@
</script> </script>
<fieldset class="search"> <fieldset class="search">
<legend>{t('search.search')}</legend> <legend>{t('search')}</legend>
<form onsubmit={doSearch}> <form onsubmit={doSearch}>
<label> <label>
{t('search.key')} {t('key')}
<input type="text" bind:value={key} /> <input type="text" bind:value={key} />
</label> </label>
<label> <label>
<input type="checkbox" bind:checked={fulltext} /> <input type="checkbox" bind:checked={fulltext} />
{t('search.fulltext')} {t('fulltext')}
</label> </label>
<button type="submit">{t('search.go')}</button> <button type="submit">{t('go')}</button>
</form> </form>
</fieldset> </fieldset>
{#if html} {#if html}
<fieldset> <fieldset>
<legend> <legend>
{t('search.results')} {t('results')}
</legend> </legend>
{@html html} {@html html}
</fieldset> </fieldset>

View File

@@ -35,13 +35,13 @@
{#if connections.length>0} {#if connections.length>0}
<fieldset tabindex="0"> <fieldset tabindex="0">
<legend>{t('user.connected_services')}</legend> <legend>{t('connected_services')}</legend>
<table> <table>
<thead> <thead>
<tr> <tr>
<th>{t('user.service')}</th> <th>{t('service')}</th>
<th>{t('user.foreign_id')}</th> <th>{t('foreign_id')}</th>
<th>{t('user.actions')}</th> <th>{t('actions')}</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@@ -50,7 +50,7 @@
<td>{connection.service_id}</td> <td>{connection.service_id}</td>
<td>{connection.foreign_id}</td> <td>{connection.foreign_id}</td>
<td> <td>
<button onclick={() => unlink(connection)}>{t('user.unlink')}</button> <button onclick={() => unlink(connection)}>{t('unlink')}</button>
</td> </td>
</tr> </tr>
{/each} {/each}

View File

@@ -6,7 +6,7 @@
let oldPass = $state(""); let oldPass = $state("");
let newPass = $state(""); let newPass = $state("");
let repeat = $state(""); let repeat = $state("");
let caption = $state(t('user.update')); let caption = $state(t('update'));
let oldEmpty = $derived(!/\S/.test(oldPass)); let oldEmpty = $derived(!/\S/.test(oldPass));
let newEmpty = $derived(!/\S/.test(newPass)); let newEmpty = $derived(!/\S/.test(newPass));
@@ -20,7 +20,7 @@
} }
async function submit(){ async function submit(){
caption = t('user.data_sent'); caption = t('data_sent');
const url = `${location.protocol}//${location.host.replace('5173','8080')}/api/user/password`; const url = `${location.protocol}//${location.host.replace('5173','8080')}/api/user/password`;
const data = { const data = {
old: oldPass, old: oldPass,
@@ -33,9 +33,9 @@
}); });
if (resp.ok){ if (resp.ok){
caption = t('user.saved'); caption = t('saved');
} else { } else {
caption = t('user.failed'); caption = t('failed');
} }
} }
</script> </script>
@@ -47,27 +47,27 @@
</style> </style>
<fieldset class="overlay"> <fieldset class="overlay">
<legend>{t('user.edit_password')}</legend> <legend>{t('edit_password')}</legend>
<label> <label>
<input type="password" bind:value={oldPass} /> {t('user.old_password')} <input type="password" bind:value={oldPass} /> {t('old_password')}
{#if oldEmpty} {#if oldEmpty}
<span class="error">{t('user.must_not_be_empty')}</span> <span class="error">{t('must_not_be_empty')}</span>
{/if} {/if}
</label> </label>
<label> <label>
<input type="password" bind:value={newPass} /> {t('user.new_password')} <input type="password" bind:value={newPass} /> {t('new_password')}
{#if newEmpty} {#if newEmpty}
<span class="error">{t('user.must_not_be_empty')}</span> <span class="error">{t('must_not_be_empty')}</span>
{/if} {/if}
</label> </label>
<label> <label>
<input type="password" bind:value={repeat} /> {t('user.repeat_new_password')} <input type="password" bind:value={repeat} /> {t('repeat_new_password')}
{#if mismatch} {#if mismatch}
<span class="error">{t('user.mismatch')}</span> <span class="error">{t('mismatch')}</span>
{/if} {/if}
</label> </label>
<button onclick={submit} disabled={sent||oldEmpty||newEmpty||mismatch}>{caption}</button> <button onclick={submit} disabled={sent||oldEmpty||newEmpty||mismatch}>{caption}</button>
<button onclick={abort} disabled={sent}>{t('user.abort')}</button> <button onclick={abort} disabled={sent}>{t('abort')}</button>
{#if error} {#if error}
<span class="error">{error}</span> <span class="error">{error}</span>
{/if} {/if}

View File

@@ -5,8 +5,8 @@
let { serviceName } = $props(); let { serviceName } = $props();
let service = $state({}) let service = $state({})
let caption = $state(t('user.save_service')); let caption = $state(t('save_service'));
let message = $state(t('user.loading_data')); let message = $state(t('loading_data'));
let router = useTinyRouter(); let router = useTinyRouter();
let disabled = $state(false); let disabled = $state(false);
@@ -23,7 +23,7 @@
}); });
async function update(){ async function update(){
caption = t('user.data_sent'); caption = t('data_sent');
const url = `${location.protocol}//${location.host.replace('5173','8080')}/api/user/oidc/${serviceName}`; const url = `${location.protocol}//${location.host.replace('5173','8080')}/api/user/oidc/${serviceName}`;
const resp = await fetch(url,{ const resp = await fetch(url,{
credentials: 'include', credentials: 'include',
@@ -31,7 +31,7 @@
body: JSON.stringify(service) body: JSON.stringify(service)
}); });
if (resp.ok){ if (resp.ok){
caption = t('user.saved'); caption = t('saved');
router.navigate('/user'); router.navigate('/user');
} else { } else {
caption = await resp.text(); caption = await resp.text();
@@ -41,30 +41,30 @@
</script> </script>
<fieldset> <fieldset>
<legend>{t('user.edit_service',serviceName)}</legend> <legend>{t('edit_service',serviceName)}</legend>
{#if service.name || !serviceName} {#if service.name || !serviceName}
<table> <table>
<tbody> <tbody>
<tr> <tr>
<th>{t('user.name')}</th> <th>{t('name')}</th>
<td> <td>
<input type="text" bind:value={service.name} /> <input type="text" bind:value={service.name} />
</td> </td>
</tr> </tr>
<tr> <tr>
<th>{t('user.client_id')}</th> <th>{t('client_id')}</th>
<td> <td>
<input type="text" bind:value={service.client_id} /> <input type="text" bind:value={service.client_id} />
</td> </td>
</tr> </tr>
<tr> <tr>
<th>{t('user.client_secret')}</th> <th>{t('client_secret')}</th>
<td> <td>
<input type="text" bind:value={service.client_secret} /> <input type="text" bind:value={service.client_secret} />
</td> </td>
</tr> </tr>
<tr> <tr>
<th>{t('user.base_url')}</th> <th>{t('base_url')}</th>
<td> <td>
<input type="text" bind:value={service.url} /> <input type="text" bind:value={service.url} />
</td> </td>
@@ -72,7 +72,7 @@
</tbody> </tbody>
</table> </table>
<button onclick={update} {disabled}>{caption}</button> <button onclick={update} {disabled}>{caption}</button>
<button onclick={() => router.navigate('/user')} {disabled}>{t('user.abort')}</button> <button onclick={() => router.navigate('/user')} {disabled}>{t('abort')}</button>
{:else} {:else}
{message} {message}
{/if} {/if}

View File

@@ -12,8 +12,8 @@
let editUser = $state(null); let editUser = $state(null);
let options = $state([]); let options = $state([]);
let sent = $state(false); let sent = $state(false);
let caption = $state(t('user.save_user')); let caption = $state(t('save_user'));
let message = $state(t('user.loading_data')); let message = $state(t('loading_data'));
onMount(async () => { onMount(async () => {
let url = `${location.protocol}//${location.host.replace('5173','8080')}/themes.json`; let url = `${location.protocol}//${location.host.replace('5173','8080')}/themes.json`;
@@ -44,7 +44,7 @@
async function save(ev){ async function save(ev){
ev.preventDefault(); ev.preventDefault();
sent = true; sent = true;
caption = t('user.data_sent'); caption = t('data_sent');
let method = 'PATCH'; let method = 'PATCH';
let url = null; let url = null;
if (user_id) { if (user_id) {
@@ -59,54 +59,54 @@
body: JSON.stringify(editUser) body: JSON.stringify(editUser)
}); });
if (resp.ok){ if (resp.ok){
caption = t('user.saved'); caption = t('saved');
checkUser(); checkUser();
router.navigate('/user'); router.navigate('/user');
} else { } else {
caption = t('user.failed'); caption = t('failed');
sent = false; sent = false;
} }
} }
</script> </script>
<fieldset> <fieldset>
<legend>{t('user.editing',user_id?user_id:'')}</legend> <legend>{t('editing',user_id?user_id:'')}</legend>
{#if editUser} {#if editUser}
<form onsubmit={save}> <form onsubmit={save}>
<table> <table>
<tbody> <tbody>
{#if editUser.id} {#if editUser.id}
<tr> <tr>
<th>{t('user.id')}</th> <th>{t('id')}</th>
<td>{editUser.id}</td> <td>{editUser.id}</td>
</tr> </tr>
{/if} {/if}
<tr> <tr>
<th>{t('user.name')}</th> <th>{t('name')}</th>
<td> <td>
<input type="text" bind:value={editUser.name} /> <input type="text" bind:value={editUser.name} />
</td> </td>
</tr> </tr>
<tr> <tr>
<th>{t('user.email')}</th> <th>{t('email')}</th>
<td> <td>
<input type="text" bind:value={editUser.email} /> <input type="text" bind:value={editUser.email} />
</td> </td>
</tr> </tr>
<tr> <tr>
<th>{t('user.language')}</th> <th>{t('language')}</th>
<td> <td>
<input type="text" bind:value={editUser.language} /> <input type="text" bind:value={editUser.language} />
</td> </td>
</tr> </tr>
<tr> <tr>
<th>{t('user.password')}</th> <th>{t('password')}</th>
<td> <td>
<input type="password" bind:value={editUser.password} /> <input type="password" bind:value={editUser.password} />
</td> </td>
</tr> </tr>
<tr> <tr>
<th>{t('user.theme')}</th> <th>{t('theme')}</th>
<td> <td>
<select bind:value={editUser.theme}> <select bind:value={editUser.theme}>
{#each options as entry,i} {#each options as entry,i}

View File

@@ -33,19 +33,19 @@
<fieldset tabindex="0"> <fieldset tabindex="0">
<legend> <legend>
{t('user.list')} {t('list')}
{#if user.permissions.includes('CREATE_USERS')} {#if user.permissions.includes('CREATE_USERS')}
<button onclick={() => router.navigate('/user/create')}>{t('user.create_new')}</button> <button onclick={() => router.navigate('/user/create')}>{t('create_new')}</button>
{/if} {/if}
</legend> </legend>
<table> <table>
<thead> <thead>
<tr> <tr>
<th>{t('user.id')}</th> <th>{t('id')}</th>
<th>{t('user.name')}</th> <th>{t('name')}</th>
<th>{t('user.email')}</th> <th>{t('email')}</th>
<th>{t('user.language')}</th> <th>{t('language')}</th>
<th>{t('user.actions')}</th> <th>{t('actions')}</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@@ -57,10 +57,10 @@
<td>{u.language}</td> <td>{u.language}</td>
<td> <td>
{#if user.permissions.includes('IMPERSONATE')} {#if user.permissions.includes('IMPERSONATE')}
<button onclick={() => impersonate(u.id)}>{t('user.impersonate')}</button> <button onclick={() => impersonate(u.id)}>{t('impersonate')}</button>
{/if} {/if}
{#if user.permissions.includes('UPDATE_USERS')} {#if user.permissions.includes('UPDATE_USERS')}
<button onclick={() => router.navigate(`/user/${u.id}/edit`)}>{t('user.edit')}</button> <button onclick={() => router.navigate(`/user/${u.id}/edit`)}>{t('edit')}</button>
{/if} {/if}
</td> </td>
</tr> </tr>

View File

@@ -45,16 +45,16 @@
<fieldset tabindex="0"> <fieldset tabindex="0">
<legend> <legend>
{t('user.login_services')} {t('login_services')}
{#if user.permissions.includes('MANAGE_LOGIN_SERVICES')} {#if user.permissions.includes('MANAGE_LOGIN_SERVICES')}
<button onclick={() => router.navigate('/user/oidc/add')}>{t('user.add_login_service')}</button> <button onclick={() => router.navigate('/user/oidc/add')}>{t('add_login_service')}</button>
{/if} {/if}
</legend> </legend>
<table> <table>
<thead> <thead>
<tr> <tr>
<th>{t('user.service')}</th> <th>{t('service')}</th>
<th>{t('user.actions')}</th> <th>{t('actions')}</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@@ -62,10 +62,10 @@
<tr> <tr>
<td>{service}</td> <td>{service}</td>
<td> <td>
<button onclick={() => connect(service)}>{t('user.connect_service')}</button> <button onclick={() => connect(service)}>{t('connect_service')}</button>
{#if user.permissions.includes('MANAGE_LOGIN_SERVICES')} {#if user.permissions.includes('MANAGE_LOGIN_SERVICES')}
<button onclick={() => router.navigate(`/user/oidc/edit/${service}`)}>{t('user.edit')}</button> <button onclick={() => router.navigate(`/user/oidc/edit/${service}`)}>{t('edit')}</button>
<button onclick={() => drop(service)}>{t('user.delete')}</button> <button onclick={() => drop(service)}>{t('delete')}</button>
{/if} {/if}
</td> </td>
</tr> </tr>

View File

@@ -6,7 +6,7 @@
const router = useTinyRouter(); const router = useTinyRouter();
let message = $state(t('user.processing_code')); let message = $state(t('processing_code'));
onMount(async () => { onMount(async () => {
let params = new URLSearchParams(location.search); let params = new URLSearchParams(location.search);

View File

@@ -11,53 +11,53 @@
</script> </script>
<fieldset> <fieldset>
<legend> <legend>
{t('user.your_profile')} {t('your_profile')}
<button onclick={() => router.navigate(`/user/${user.id}/edit`)}>{t('user.edit')}</button> <button onclick={() => router.navigate(`/user/${user.id}/edit`)}>{t('edit')}</button>
<button onclick={() => router.navigate(`/message/settings`)}>{t('messages.settings')}</button> <button onclick={() => router.navigate(`/message/settings`)}>{t('settings')}</button>
</legend> </legend>
<table> <table>
<tbody> <tbody>
<tr> <tr>
<th>{t('user.id')}</th> <th>{t('id')}</th>
<td>{user.id}</td> <td>{user.id}</td>
</tr> </tr>
<tr> <tr>
<th>{t('user.name')}</th> <th>{t('name')}</th>
<td>{user.name}</td> <td>{user.name}</td>
</tr> </tr>
<tr> <tr>
<th>{t('user.login')}</th> <th>{t('login')}</th>
<td>{user.login}</td> <td>{user.login}</td>
</tr> </tr>
<tr> <tr>
<th>{t('user.email')}</th> <th>{t('email')}</th>
<td>{user.email}</td> <td>{user.email}</td>
</tr> </tr>
<tr> <tr>
<th>{t('user.language')}</th> <th>{t('language')}</th>
<td>{user.language}</td> <td>{user.language}</td>
</tr> </tr>
<tr> <tr>
<th>{t('user.theme')}</th> <th>{t('theme')}</th>
<td>{user.theme}</td> <td>{user.theme}</td>
</tr> </tr>
<tr> <tr>
<th>{t('user.password')}</th> <th>{t('password')}</th>
<td> <td>
{#if editPassword} {#if editPassword}
<EditPassword bind:editPassword={editPassword} /> <EditPassword bind:editPassword={editPassword} />
{:else} {:else}
<button onclick={() => editPassword = true}>{t('user.edit_password')}</button> <button onclick={() => editPassword = true}>{t('edit_password')}</button>
{/if} {/if}
</td> </td>
</tr> </tr>
<tr> <tr>
<th>{t('user.permissions')}</th> <th>{t('permissions')}</th>
<td> <td>
<ul> <ul>
{#each user.permissions as permission,i} {#each user.permissions as permission,i}
<li>{t('user.'+permission)}</li> <li>{t(permission)}</li>
{/each} {/each}
</ul> </ul>
</td> </td>

View File

@@ -4,7 +4,7 @@
import { useTinyRouter } from 'svelte-tiny-router'; import { useTinyRouter } from 'svelte-tiny-router';
let mail = ""; let mail = "";
let caption = t('user.send_mail'); let caption = t('send_mail');
let error = null; let error = null;
const router = useTinyRouter(); const router = useTinyRouter();
@@ -16,7 +16,7 @@
body : mail body : mail
}); });
if (resp.ok) { if (resp.ok) {
caption = t('user.data_sent'); caption = t('data_sent');
} else { } else {
caption = await resp.text(); caption = await resp.text();
} }
@@ -58,10 +58,10 @@
<form onsubmit={submit}> <form onsubmit={submit}>
<fieldset> <fieldset>
<legend>{t('user.reset_pw')}</legend> <legend>{t('reset_pw')}</legend>
<label> <label>
<input type="email" bind:value={mail}/> <input type="email" bind:value={mail}/>
{t('user.enter_email')} {t('enter_email')}
</label> </label>
<button type="submit">{caption}</button> <button type="submit">{caption}</button>
{#if error} {#if error}

View File

@@ -14,7 +14,7 @@
} }
</script> </script>
<h1>{t('user.user_module')}</h1> <h1>{t('user_module')}</h1>
<Profile /> <Profile />
<Services /> <Services />

View File

@@ -1,37 +1,115 @@
{ {
"contacts": {
"loading": "lade…"
},
"document": {
"abort": "abbrechen", "abort": "abbrechen",
"actions": "Aktionen", "actions": "Aktionen",
"add_login_service": "Login-Service anlegen",
"add_new": "{0} anlegen", "add_new": "{0} anlegen",
"add_position": "hinzufügen", "add_position": "hinzufügen",
"advertisement" : "Umbrella ist ein Produkt von {0}.",
"amount": "Menge", "amount": "Menge",
"bank_account": "Bankverbindung", "bank_account": "Bankverbindung",
"base_url": "Basis-URL",
"bookmark": "Lesezeichen",
"client_id": "Client-ID",
"client_secret": "Client-Geheimnis",
"code": "Code", "code": "Code",
"connect_service": "mit Service verbinden",
"connected_services": "verbundene Login-Services",
"confirm_deletion": "Soll '{pos}' wirklich gelöscht werden?", "confirm_deletion": "Soll '{pos}' wirklich gelöscht werden?",
"court": "Amtsgericht", "company": "Firma",
"create_new": "neues Dokument", "contact": "Kontakte",
"customer": "Kunde", "contained_tax": "enthaltene Steuer",
"content": "Inhalt",
"create_new_document": "neues Dokument",
"create_new_user": "Neuen Benutzer anlegen",
"CREATE_USERS": "Nutzer anlegen",
"create_pdf": "PDF erzeugen",
"customer_address": "Adresse", "customer_address": "Adresse",
"customer_email": "Emailadresse des Kunden",
"customer": "Kunde",
"customer_id": "Kundennummer", "customer_id": "Kundennummer",
"data_sent": "Daten übermittelt",
"date": "Datum", "date": "Datum",
"delete": "löschen", "delete": "löschen",
"DELETE_USERS": "Nutzer löschen",
"delivery_date": "Lieferdatum",
"description": "Beschreibung",
"document": "Dokumente",
"documents": "Dokumente",
"do_login" : "anmelden",
"edit": "Bearbeiten",
"editing": "Nutzer {0} bearbeiten",
"edit_password": "Passwort ändern",
"edit_service": "Login-Service \"{0}\" bearbeiten",
"email": "E-Mail", "email": "E-Mail",
"email_or_username": "Email oder Nutzername",
"estimated_time": "geschätzte Zeit", "estimated_time": "geschätzte Zeit",
"estimated_times": "geschätzte Zeiten", "estimated_times": "geschätzte Zeiten",
"failed": "fehlgeschlagen",
"files": "Dateien",
"footer": "Fuß-Text", "footer": "Fuß-Text",
"foreign_id": "externe Kennung",
"forgot_pass" : "Password vergessen?",
"fulltext": "Volltextsuche",
"go": "los!",
"go_to_url_to_reset_password": "Um ein neues Passwort zu erhalten, öffnen Sie bitte den folgenden Link: {url}",
"gross_sum": "Brutto-Summe", "gross_sum": "Brutto-Summe",
"head": "Kopf-Text", "head": "Kopf-Text",
"hours": "Stunden", "hours": "Stunden",
"id": "Id",
"impersonate": "zu Nutzer wechseln",
"IMPERSONATE": "Nutzer wechseln",
"invoice": "Rechnung",
"items": "Artikel", "items": "Artikel",
"key": "Suchbegriff",
"language": "Sprache",
"list": "Dokumente", "list": "Dokumente",
"list_of": "Dokumente von {0}", "list_of": "Dokumente von {0}",
"LIST_USERS": "Nutzer auflisten",
"loading": "lade…",
"loading_data": "Daten werden geladen…",
"local_court": "Amtsgericht",
"login" : "Anmeldung",
"login_services": "Login-Services",
"logout": "Abmelden",
"MANAGE_LOGIN_SERVICES": "Login-Services verwalten",
"messages": "Benachrichtigungen",
"model": "Modelle",
"mismatch": "ungleich",
"must_not_be_empty": "darf nicht leer sein",
"name": "Name",
"net_price": "Nettopreis", "net_price": "Nettopreis",
"net_sum": "Netto-Summe",
"new_password": "neues Passwort",
"notes": "Notizen",
"number": "Nummer", "number": "Nummer",
"oidc_Login" : "Anmeldung mit OIDC",
"old_password": "altes Passwort",
"password" : "Passwort",
"permissions": "Berechtigungen",
"pos": "Pos", "pos": "Pos",
"position": "Position",
"positions": "Positionen", "positions": "Positionen",
"price": "Preis",
"processing_code": "Code wird verarbeitet…",
"project": "Projekte",
"repeat_new_password": "Wiederholung",
"results": "Ergebnisse",
"saved": "gespeichert",
"save_service": "Service speichern",
"save_user": "Nutzer speichern",
"search": "Suche",
"select_company" : "Wählen Sie eine ihrer Firmen:", "select_company" : "Wählen Sie eine ihrer Firmen:",
"select_customer": "Kunde auswählen", "select_customer": "Kunde auswählen",
"sender": "Absender", "sender": "Absender",
@@ -39,6 +117,9 @@
"sender_local_court": "Amtsgericht", "sender_local_court": "Amtsgericht",
"sender_name": "Name", "sender_name": "Name",
"sender_tax_id": "Steuernummer", "sender_tax_id": "Steuernummer",
"sent_email": "Email gesendet",
"service": "Service",
"settings" : "Eisntellungen",
"state": "Status", "state": "Status",
"state_declined": "abgelehnt", "state_declined": "abgelehnt",
"state_delayed": "verspätet", "state_delayed": "verspätet",
@@ -46,117 +127,40 @@
"state_new":"neu", "state_new":"neu",
"state_payed": "bezahlt", "state_payed": "bezahlt",
"state_sent": "versendet", "state_sent": "versendet",
"tax_id": "Steuernummer",
"tax_rate": "Steuersatz",
"timetrack": "Zeiterfassung",
"title_or_desc": "Titel/Beschreibung",
"type": "Dokumententyp",
"type_confirmation": "Bestätigung",
"type_invoice": "Rechnung",
"type_offer": "Angebot",
"type_reminder": "Erinnerung",
"unit": "Einheit",
"unit_price": "Preis/Einheit"
},
"footer": {
"message" : "Umbrella ist ein Produkt von {0}."
},
"home" : {
"Welcome" : "Willkommen, {0}"
},
"items": {
"items": "Artikel"
},
"login" : {
"do_login" : "anmelden",
"Email_or_Username": "Email oder Nutzername",
"forgot_pass" : "Password vergessen?",
"Login" : "Anmeldung",
"OIDC_Login" : "Anmeldung mit OIDC",
"Password" : "Passwort"
},
"menu" : {
"bookmark": "Lesezeichen",
"company": "Firma",
"contact": "Kontakte",
"document": "Dokumente",
"documents": "Dokumente",
"files": "Dateien",
"items": "Items",
"logout": "Abmelden",
"message": "Benachrichtigungen",
"model": "Modelle",
"notes": "Notizen",
"project": "Projekte",
"stock": "Inventar",
"task": "Aufgaben",
"time": "Zeiterfassung",
"tutorial": "Tutorial",
"user": "Benutzer",
"users": "Benutzer",
"wiki": "Wiki"
},
"status" : { "status" : {
"403": "Zugriff verweigert", "403": "Zugriff verweigert",
"404": "Seite nicht gefunden", "404": "Seite nicht gefunden",
"501": "Nicht implementiert" "501": "Nicht implementiert"
}, },
"task": { "stock": "Inventar",
"estimated_times": "geschätzte Zeiten"
}, "task": "Aufgaben",
"user" : { "tax_id": "Steuernummer",
"actions": "Aktionen", "tax_rate": "Steuersatz",
"abort": "abbrechen",
"add_login_service": "Login-Service anlegen",
"base_url": "Basis-URL",
"client_id": "Client-ID",
"client_secret": "Client-Geheimnis",
"connect_service": "mit Service verbinden",
"connected_services": "verbundene Login-Services",
"create_new": "Neuen Benutzer anlegen",
"CREATE_USERS": "Nutzer anlegen",
"data_sent": "Daten übermittelt",
"delete": "löschen",
"DELETE_USERS": "Nutzer löschen",
"edit": "Bearbeiten",
"editing": "Nutzer {0} bearbeiten",
"edit_password": "Passwort ändern",
"edit_service": "Login-Service \"{0}\" bearbeiten",
"email": "E-Mail",
"failed": "fehlgeschlagen",
"foreign_id": "externe Kennung",
"go_to_url_to_reset_password": "Um ein neues Passwort zu erhalten, öffnen Sie bitte den folgenden Link: {url}",
"id": "Id",
"impersonate": "zu Nutzer wechseln",
"IMPERSONATE": "Nutzer wechseln",
"language": "Sprache",
"list": "Benutzer-Liste",
"LIST_USERS": "Nutzer auflisten",
"loading_data": "Daten werden geladen…",
"login": "Login",
"login_services": "Login-Services",
"MANAGE_LOGIN_SERVICES": "Login-Services verwalten",
"mismatch": "ungleich",
"must_not_be_empty": "darf nicht leer sein",
"name": "Name",
"new_password": "neues Passwort",
"old_password": "altes Passwort",
"password": "Passwort",
"permissions": "Berechtigungen",
"processing_code": "Code wird verarbeitet…",
"repeat_new_password": "Wiederholung",
"saved": "gespeichert",
"save_service": "Service speichern",
"save_user": "Nutzer speichern",
"sent_email": "Email gesendet",
"service": "Service",
"settings" : "Eisntellungen",
"theme": "Design", "theme": "Design",
"timetrack": "Zeiterfassung",
"title_or_desc": "Titel/Beschreibung",
"tutorial": "Tutorial",
"type": "Dokumententyp",
"type_confirmation": "Bestätigung",
"type_invoice": "Rechnung",
"type_offer": "Angebot",
"type_reminder": "Erinnerung",
"unit": "Einheit",
"unit_price": "Preis/Einheit",
"unlink": "Trennen", "unlink": "Trennen",
"update": "aktualisieren", "update": "aktualisieren",
"UPDATE_USERS" : "Nutzer aktualisieren", "UPDATE_USERS" : "Nutzer aktualisieren",
"user_module" : "Umbrella User-Verwaltung", "user_module" : "Umbrella User-Verwaltung",
"user": "Benutzer",
"user_list": "Benutzer-Liste",
"users": "Benutzer",
"welcome" : "Willkommen, {0}",
"wiki": "Wiki",
"your_password_reset_token" : "Ihr Token zum Erstellen eines neuen Passworts", "your_password_reset_token" : "Ihr Token zum Erstellen eines neuen Passworts",
"your_profile": "dein Profil" "your_profile": "dein Profil"
} }
}