working on address card editor

This commit is contained in:
2025-10-08 22:16:35 +02:00
parent 2f3a29d606
commit c888845191
14 changed files with 148 additions and 50 deletions

View File

@@ -11,6 +11,7 @@ import static de.srsoftware.umbrella.core.exceptions.UmbrellaException.notFound;
import static java.lang.System.Logger.Level.ERROR; import static java.lang.System.Logger.Level.ERROR;
import static java.text.MessageFormat.format; import static java.text.MessageFormat.format;
import de.srsoftware.tools.jdbc.Query;
import de.srsoftware.umbrella.core.BaseDb; import de.srsoftware.umbrella.core.BaseDb;
import de.srsoftware.umbrella.core.exceptions.UmbrellaException; import de.srsoftware.umbrella.core.exceptions.UmbrellaException;
import de.srsoftware.umbrella.core.model.Contact; import de.srsoftware.umbrella.core.model.Contact;
@@ -90,7 +91,13 @@ public class SqliteDb extends BaseDb implements ContactDb{
@Override @Override
public Contact save(Contact contact) { public Contact save(Contact contact) {
LOG.log(ERROR,"Save not implemented!"); if (contact.id() == 0){ // new contact
throw UmbrellaException.unprocessable("Storing of new contacts not implemented");
} else try { // update
Query.update(TABLE_CONTACTS).set(DATA).where(ID,equal(contact.id())).prepare(db).apply(contact.vcard()).execute();
} catch (SQLException e) {
throw databaseException("Failed to update vcard {0}",contact.id());
}
return contact; return contact;
} }
} }

View File

@@ -8,6 +8,7 @@
href = '#', href = '#',
onclick = evt => { evt.preventDefault(); startEdit(); return false }, onclick = evt => { evt.preventDefault(); startEdit(); return false },
onSet = newVal => {return true;}, onSet = newVal => {return true;},
title = t('long_click_to_edit'),
type = 'div', type = 'div',
value = $bindable(null) value = $bindable(null)
} = $props(); } = $props();
@@ -103,5 +104,5 @@
{#if editable && editing} {#if editable && editing}
<input bind:value={editValue} onkeyup={typed} autofocus /> <input bind:value={editValue} onkeyup={typed} autofocus />
{:else} {:else}
<svelte:element this={type} href={href} onclick={ignore} {onmousedown} {onmouseup} {ontouchstart} {ontouchend} {oncontextmenu} class={{editable}} title={t('long_click_to_edit')} >{value}</svelte:element> <svelte:element this={type} href={href} onclick={ignore} {onmousedown} {onmouseup} {ontouchstart} {ontouchend} {oncontextmenu} class={{editable}} {title} >{value}</svelte:element>
{/if} {/if}

View File

@@ -1,31 +1,46 @@
<script> <script>
import LineEditor from '../../Components/LineEditor.svelte';
import { addr } from '../../vcard.js'; import { addr } from '../../vcard.js';
import { t } from '../../translations.svelte';
let { code } = $props(); let { code, patch = (from, to) => true } = $props();
let address = $derived(addr(code)); let address = $derived(addr(code));
let home = $derived(code.toLowerCase().includes('type=home'));
let work = $derived(code.toLowerCase().includes('type=work'));
function onSet(oldVal,newVal){
const newCode = code.replace(oldVal,newVal);
return patch(code,newCode);
}
function toggleHome(){
toggleType('HOME');
}
function toggleType(key){
key = key.toUpperCase();
if (code.toUpperCase().includes(';TYPE='+key)) {
const regex = new RegExp(';TYPE='+key, "ig");
patch(code,code.replace(regex,''));
} else patch(code,code.replace('ADR','ADR;TYPE='+key));
}
function toggleWork(){
toggleType('WORK');
}
</script> </script>
<div class="address"> <div class="address">
{#if address.box} <div>
<span class="post_box">{address.box}</span> <span class="symbol {home?'':'inactive'}" onclick={toggleHome}></span>
{/if} <span class="symbol {work?'':'inactive'}" onclick={toggleWork} ></span>
{#if address.ext} </div>
<span class="extended">{address.ext}</span> <LineEditor type="span" editable={true} value={address.box} onSet={newVal => onSet(address.box,newVal)} title={t('post_box')} />
{/if} <LineEditor type="span" editable={true} value={address.ext} onSet={newVal => onSet(address.ext,newVal)} title={t('extended_address')} />
{#if address.street} <LineEditor type="span" editable={true} value={address.street} onSet={newVal => onSet(address.street,newVal)} title={t('street')} />
<span class="street">{address.street}</span> <LineEditor type="span" editable={true} value={address.code} onSet={newVal => onSet(address.code,newVal)} title={t('post_code')} />
{/if} <LineEditor type="span" editable={true} value={address.loc} onSet={newVal => onSet(address.loc,newVal)} title={t('locality')} />
{#if address.code} <LineEditor type="span" editable={true} value={address.region} onSet={newVal => onSet(address.region,newVal)} title={t('region')} />
<span class="code">{address.code}</span> <LineEditor type="span" editable={true} value={address.country} onSet={newVal => onSet(address.country,newVal)} title={t('country')} />
{/if}
{#if address.loc}
<span class="locality">{address.loc}</span>
{/if}
{#if address.region}
<span class="region">{address.region}</span>
{/if}
{#if address.country}
<span class="country">{address.country}</span>
{/if}
</div> </div>

View File

@@ -15,12 +15,12 @@
let addresses = $derived(contact.vcard.match(/^ADR.*:.+$/gm)); let addresses = $derived(contact.vcard.match(/^ADR.*:.+$/gm));
let code = $state(false); let code = $state(false);
let fns = $derived(contact.vcard.match(/^FN.*:.+$/gm));
let emails = $derived(contact.vcard.match(/^EMAIL.*:.+$/gm)); let emails = $derived(contact.vcard.match(/^EMAIL.*:.+$/gm));
let extra_fields = $derived(contact.vcard.match(/^X-.*:.+/gm)); let extra_fields = $derived(contact.vcard.match(/^X-.*:.+/gm));
let fns = $derived(contact.vcard.match(/^FN.*:.+$/gm));
let orgs = $derived(contact.vcard.match(/^ORG.*:.+$/gm));
async function patch(from,to){ async function patch(from,to){
console.log(`patch(${contact.id}: ${from}${to})`);
if (from == to) return; if (from == to) return;
const url = api(`contact/${contact.id}`); const url = api(`contact/${contact.id}`);
const res = await fetch(url,{ const res = await fetch(url,{
@@ -64,7 +64,9 @@
</tr> </tr>
<tr> <tr>
<td> <td>
<Org vcard={contact.vcard} /> {#each orgs as code}
<Org {code} {patch} />
{/each}
</td> </td>
</tr> </tr>
<tr> <tr>
@@ -75,21 +77,21 @@
<tr> <tr>
<td> <td>
{#each addresses as code} {#each addresses as code}
<Address {code} /> <Address {code} {patch} />
{/each} {/each}
</td> </td>
</tr> </tr>
<tr> <tr>
<td> <td>
{#each emails as code} {#each emails as code}
<Email {code} /> <Email {code} {patch} />
{/each} {/each}
</td> </td>
</tr> </tr>
<tr> <tr>
<td> <td>
{#each extra_fields as code} {#each extra_fields as code}
<ExtraField {code} /> <ExtraField {code} {patch} />
{/each} {/each}
</td> </td>
</tr> </tr>

View File

@@ -1,9 +1,37 @@
<script> <script>
import LineEditor from '../../Components/LineEditor.svelte';
import { email } from '../../vcard.js'; import { email } from '../../vcard.js';
let { code } = $props(); let { code, patch = (from, to) => true } = $props();
let adr = $derived(email(code)); let home = $derived(code.toLowerCase().includes('type=home'));
let work = $derived(code.toLowerCase().includes('type=work'));
let value = $derived(email(code));
function onSet(newVal){
const newCode = code.replace(value,newVal);
return patch(code,newCode);
}
function toggleHome(){
toggleType('HOME');
}
function toggleType(key){
key = key.toUpperCase();
if (code.toUpperCase().includes(';TYPE='+key)) {
const regex = new RegExp(';TYPE='+key, "ig");
patch(code,code.replace(regex,''));
} else patch(code,code.replace('EMAIL','EMAIL;TYPE='+key));
}
function toggleWork(){
toggleType('WORK');
}
</script> </script>
<span class="email">{adr}</span> {#if value}
<span class="symbol {home?'':'inactive'}" onclick={toggleHome}></span>
<span class="symbol {work?'':'inactive'}" onclick={toggleWork} ></span>
<LineEditor type="span" editable={true} {value} {onSet} /><br/>
{/if}

View File

@@ -1,14 +1,25 @@
<script> <script>
import LineEditor from '../../Components/LineEditor.svelte';
import MultiLineEditor from '../../Components/MultilineEditor.svelte';
import { extra } from '../../vcard.js'; import { extra } from '../../vcard.js';
import { t } from '../../translations.svelte';
let { code } = $props(); let { code, patch = (from, to) => true } = $props();
let field = $derived(extra(code)); let field = $derived(extra(code));
function onSet(newVal){
const newCode = code.replace(field.value,newVal.replaceAll('\n','\\n'));
return patch(code,newCode);
}
</script> </script>
{#if field} {#if field}
<span class={field.name}> <div class={field.name}>
{field.value} {#if field.value.includes('\\n')}
</span> <MultiLineEditor type="div" editable={true} value={field.value.replaceAll('\\n','\n')} {onSet} title={t(field.name)+' '+t('long_click_to_edit')} />
{:else}
<LineEditor type="div" editable={true} value={field.value} {onSet} title={t(field.name)+' '+t('long_click_to_edit')} />
{/if}
</div>
{/if} {/if}

View File

@@ -1,18 +1,18 @@
<script> <script>
import LineEditor from '../../Components/LineEditor.svelte'; import LineEditor from '../../Components/LineEditor.svelte';
import { fn } from '../../vcard.js'; import { fn } from '../../vcard.js';
import { t } from '../../translations.svelte'; import { t } from '../../translations.svelte';
let { code, patch = (from, to) => true } = $props(); let { code, patch = (from, to) => true } = $props();
let name = $derived(fn(code)); let value = $derived(fn(code));
function onSet(newVal){ function onSet(newVal){
const newCode = code.replace(name,newVal); const newCode = code.replace(value,newVal);
return patch(code,newCode); return patch(code,newCode);
} }
</script> </script>
{#if name} {#if value}
<LineEditor type="span" editable={true} value={name} {onSet} /> <LineEditor type="span" editable={true} {value} {onSet} title={t('formatted_name')}/>
{/if} {/if}

View File

@@ -18,7 +18,6 @@
yikes(); yikes();
var data = await res.json(); var data = await res.json();
contacts = Object.values(data).sort(byName); contacts = Object.values(data).sort(byName);
console.log(contacts);
} else { } else {
error(res); error(res);
} }

View File

@@ -1,12 +1,18 @@
<script> <script>
import LineEditor from '../../Components/LineEditor.svelte';
import { org } from '../../vcard.js'; import { org } from '../../vcard.js';
import { t } from '../../translations.svelte'; import { t } from '../../translations.svelte';
let { vcard } = $props(); let { code, patch = (from, to) => true } = $props();
let o = $derived(org(vcard)); let value = $derived(org(code));
function onSet(newVal){
const newCode = code.replace(value,newVal);
return patch(code,newCode);
}
</script> </script>
{#if o} {#if value}
<span class="organization">{o}</span> <LineEditor type="span" editable={true} {value} {onSet} title={t('organization')}/>
{/if} {/if}

View File

@@ -28,7 +28,7 @@ export function email(vcard){
} }
export function extra(code){ export function extra(code){
const match = code.match(/^X-(.+):(.+)/) const match = code.match(/^X-([^:]+):(.+)/)
return match ? {name:match[1],value:match[2]} : null return match ? {name:match[1],value:match[2]} : null
} }

View File

@@ -38,6 +38,9 @@
"contained_tax": "enthaltene Steuer", "contained_tax": "enthaltene Steuer",
"content": "Inhalt", "content": "Inhalt",
"context": "Kontext", "context": "Kontext",
"country": "Land",
"COURT": "Amtsgericht",
"CUSTOMER-NUMBER": "Kundennummer",
"customer_number_prefix": "Präfix für Kundennummer", "customer_number_prefix": "Präfix für Kundennummer",
"create": "anlegen", "create": "anlegen",
"create_new_object": "{object} neu anlegen", "create_new_object": "{object} neu anlegen",
@@ -94,6 +97,7 @@
"footer": "Fuß-Text", "footer": "Fuß-Text",
"foreign_id": "externe Kennung", "foreign_id": "externe Kennung",
"forgot_pass" : "Password vergessen?", "forgot_pass" : "Password vergessen?",
"formatted_name": "Anzeigename",
"fulltext": "Volltextsuche", "fulltext": "Volltextsuche",
"go": "los!", "go": "los!",
@@ -122,6 +126,7 @@
"loading_data": "Daten werden geladen…", "loading_data": "Daten werden geladen…",
"loading_object": "lade {object}…", "loading_object": "lade {object}…",
"local_court": "Amtsgericht", "local_court": "Amtsgericht",
"locality": "Ort",
"login" : "Anmeldung", "login" : "Anmeldung",
"login_services": "Login-Services", "login_services": "Login-Services",
"logout": "Abmelden", "logout": "Abmelden",
@@ -172,6 +177,7 @@
"oidc_Login" : "Anmeldung mit OIDC", "oidc_Login" : "Anmeldung mit OIDC",
"old_password": "altes Passwort", "old_password": "altes Passwort",
"organization": "Organisation",
"page": "Seite", "page": "Seite",
"parent_task": "übergeordnete Aufgabe", "parent_task": "übergeordnete Aufgabe",
@@ -189,6 +195,7 @@
"pos": "Pos", "pos": "Pos",
"position": "Position", "position": "Position",
"positions": "Positionen", "positions": "Positionen",
"post_code": "Postleitzahl",
"postpone": "aufschieben", "postpone": "aufschieben",
"price": "Preis", "price": "Preis",
"priority": "Priorität", "priority": "Priorität",
@@ -197,6 +204,7 @@
"projects": "Projekte", "projects": "Projekte",
"record": "Eintrag", "record": "Eintrag",
"region": "Bundesland",
"repeat_new_password": "Wiederholung", "repeat_new_password": "Wiederholung",
"results": "Ergebnisse", "results": "Ergebnisse",
@@ -241,6 +249,7 @@
"501": "Nicht implementiert" "501": "Nicht implementiert"
}, },
"stock": "Inventar", "stock": "Inventar",
"street": "Straße",
"subject": "Betreff", "subject": "Betreff",
"subtask": "Unteraufgabe", "subtask": "Unteraufgabe",
"subtasks": "Unteraufgaben", "subtasks": "Unteraufgaben",
@@ -254,6 +263,7 @@
"task_list": "Aufgabenliste", "task_list": "Aufgabenliste",
"tasks": "Aufgaben", "tasks": "Aufgaben",
"tax_id": "Steuernummer", "tax_id": "Steuernummer",
"TAX-NUMBER": "Steuernummer",
"tax_rate": "Steuersatz", "tax_rate": "Steuersatz",
"template": "Vorlage", "template": "Vorlage",
"theme": "Design", "theme": "Design",

View File

@@ -38,6 +38,9 @@
"contained_tax": "contained tax", "contained_tax": "contained tax",
"content": "content", "content": "content",
"context": "context", "context": "context",
"country": "country",
"COURT": "local court",
"CUSTOMER-NUMBER": "customer number",
"customer_number_prefix": "prefix for customer number", "customer_number_prefix": "prefix for customer number",
"create": "create", "create": "create",
"create_new_object": "create {object}", "create_new_object": "create {object}",
@@ -88,11 +91,13 @@
"failed": "failed", "failed": "failed",
"failed_login_attempts" : "account locked until {release_time} after {attempts} failed login attempts", "failed_login_attempts" : "account locked until {release_time} after {attempts} failed login attempts",
"file": "file",
"files": "files", "files": "files",
"filter": "filter", "filter": "filter",
"footer": "footer", "footer": "footer",
"foreign_id": "external ID", "foreign_id": "external ID",
"forgot_pass" : "forgot password?", "forgot_pass" : "forgot password?",
"formatted_name": "display name",
"fulltext": "full text search", "fulltext": "full text search",
"go": "go!", "go": "go!",
@@ -121,6 +126,7 @@
"loading_data": "loading data…", "loading_data": "loading data…",
"loading_object": "loading {object}…", "loading_object": "loading {object}…",
"local_court": "local court", "local_court": "local court",
"locality": "locality",
"login" : "login", "login" : "login",
"login_services": "login service", "login_services": "login service",
"logout": "logout", "logout": "logout",
@@ -171,6 +177,7 @@
"oidc_Login" : "Login via OIDC", "oidc_Login" : "Login via OIDC",
"old_password": "old password", "old_password": "old password",
"organization": "organization",
"page": "page", "page": "page",
"parent_task": "parent task", "parent_task": "parent task",
@@ -188,6 +195,7 @@
"pos": "pos", "pos": "pos",
"position": "position", "position": "position",
"positions": "positions", "positions": "positions",
"post_code": "post cdoe",
"postpone": "postpone", "postpone": "postpone",
"price": "price", "price": "price",
"priority": "priority", "priority": "priority",
@@ -196,6 +204,7 @@
"projects": "projects", "projects": "projects",
"record": "record", "record": "record",
"region": "region",
"repeat_new_password": "repeat new password", "repeat_new_password": "repeat new password",
"results": "results", "results": "results",
@@ -240,6 +249,7 @@
"501": "Not implemented" "501": "Not implemented"
}, },
"stock": "stock", "stock": "stock",
"street": "street",
"subject": "subject", "subject": "subject",
"subtask": "subtask", "subtask": "subtask",
"subtasks": "subtasks", "subtasks": "subtasks",
@@ -253,6 +263,7 @@
"task_list": "task list", "task_list": "task list",
"tasks": "tasks", "tasks": "tasks",
"tax_id": "tax ID", "tax_id": "tax ID",
"TAX-NUMBER": "tax ID",
"tax_rate": "tax rate", "tax_rate": "tax rate",
"template": "template", "template": "template",
"theme": "design", "theme": "design",

View File

@@ -239,4 +239,8 @@ textarea{
.kanban .state_20 .box.p100, .kanban .state_20 .box.p100,
.kanban .state_40 .box.p100{ .kanban .state_40 .box.p100{
border: 5px solid #733440; border: 5px solid #733440;
}
.vcard span.inactive{
color: #222200;
} }

View File

@@ -402,8 +402,12 @@ fieldset.vcard{
.vcard .name{ .vcard .name{
font-weight: bold; font-weight: bold;
} }
.vcard div.address{
display: inline-block;
margin-right: 10px;
}
.vcard .address span { .vcard .address > span {
display: block; display: block;
} }
.vcard table{ .vcard table{