Compare commits

..
11 Commits
Author SHA1 Message Date
StephanRichter cd32779f9f extended documentation
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2026-07-02 09:15:31 +02:00
StephanRichter 1b2cef5bd8 extended documentation
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2026-07-02 09:15:31 +02:00
StephanRichter de0d9c1797 working on task description
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2026-07-02 09:15:31 +02:00
StephanRichter db66097253 working on demodata
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2026-07-02 09:15:31 +02:00
StephanRichter 92faa364c9 adding demodata
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2026-07-02 09:15:31 +02:00
StephanRichter 66bd0e3741 working on demodata
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2026-07-02 09:15:31 +02:00
StephanRichter c61a2b14b6 extended demo data
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2026-07-02 09:15:31 +02:00
StephanRichter 57bce84e8e extended demo data
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2026-07-02 09:15:31 +02:00
StephanRichter 6e9d32f105 extending demo data
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2026-07-02 09:15:31 +02:00
StephanRichter 6d9e368e43 started creating demo data
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2026-07-02 09:15:31 +02:00
StephanRichter b5b361514d re-implemented new transaction form
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2026-07-02 09:15:31 +02:00
64 changed files with 367 additions and 378 deletions
@@ -16,7 +16,7 @@ public interface AccountDb {
Collection<UmbrellaUser> getMembers(long accountId);
Optional<Transaction> lastTransaction(long accountId, String source, String dest, double amount);
Optional<Transaction> lastTransaction(long accountId, String source, String destination, Double amount);
Collection<Account> listAccounts(long userId);
@@ -311,12 +311,11 @@ public class AccountingModule extends BaseHandler implements AccountingService {
var source = src.get(src.has(Field.ID) ? Field.ID : Field.DISPLAY).toString();
if (!json.has(Field.DESTINATION)) throw missingField(Field.DESTINATION);
if (!(json.get(Field.DESTINATION) instanceof JSONObject dst)) throw invalidField(Field.SOURCE,JSON);
var dest = dst.get(dst.has(Field.ID) ? Field.ID : Field.DISPLAY).toString();
if (!json.has(Field.AMOUNT)) throw missingField(Field.AMOUNT);
if (!(json.get(Field.AMOUNT) instanceof Number amt)) throw invalidField(Field.AMOUNT,Text.NUMBER);
var amount = amt.doubleValue();
String destination = dst.has(Field.ID) ? dst.get(Field.ID).toString() : dst.has(Field.DISPLAY) ? dst.get(Field.DISPLAY).toString() : null;
Double amount = null;
if (json.has(Field.AMOUNT) && json.get(Field.AMOUNT) instanceof Number amt) amount = amt.doubleValue();
var transaction = accountDb.lastTransaction(accountId, source, dest, amount);
var transaction = accountDb.lastTransaction(accountId, source, destination, amount);
return transaction.isPresent() ? sendContent(ex,transaction.get()) : notFound(ex);
}
@@ -124,16 +124,13 @@ public class SqliteDb extends BaseDb implements AccountDb {
db.setAutoCommit(false);
Query.delete().from(TABLE_TAGS_TRANSACTIONS).where(TRANSACTION_ID,equal(transaction.id())).execute(db);
Query.delete().from(TABLE_TRANSACTIONS).where(ID,equal(transaction.id())).execute(db);
db.setAutoCommit(true);
return transaction;
} catch (SQLException e){
try {
db.rollback();
} catch (SQLException ignored){};
throw failedToDropObject(transaction);
} finally {
try {
db.setAutoCommit(true);
} catch (SQLException ignored){};
}
}
@@ -173,22 +170,42 @@ public class SqliteDb extends BaseDb implements AccountDb {
}
@Override
public Optional<Transaction> lastTransaction(long accountId, String source, String dest, double amount) {
public Optional<Transaction> lastTransaction(long accountId, String source, String destination, Double amount) {
try {
var rs = select(ALL).from(TABLE_TRANSACTIONS)
.where(ACCOUNT,equal(accountId)).where(SOURCE,equal(source)).where(DESTINATION,equal(dest)).where(AMOUNT,equal(amount))
.sort(ID+" DESC")
.limit(1)
.exec(db);
var query = select(ALL).from(TABLE_TRANSACTIONS).where(ACCOUNT,equal(accountId));
if (source != null) query = query.where(SOURCE,equal(source));
if (destination != null) query = query.where(DESTINATION,equal(destination));
if (amount != null) query = query.where(AMOUNT,equal(amount));
var rs = query.sort(ID+" DESC").limit(1).exec(db);
Transaction ta = null;
if (rs.next()) ta = Transaction.of(rs);
rs.close();
if (ta == null && amount != null) { // try to search by amount, ignore source and dest
rs = select(ALL).from(TABLE_TRANSACTIONS).where(ACCOUNT, equal(accountId)).where(AMOUNT, equal(amount))
.sort(ID + " DESC").limit(1).exec(db);
if (rs.next()) ta = Transaction.of(rs);
rs.close();
}
if (ta == null && source != null && destination != null) { // try to search by amount, ignore source and dest
rs = select(ALL).from(TABLE_TRANSACTIONS)
.where(SOURCE,equal(source))
.where(DESTINATION,equal(destination))
.where(ACCOUNT, equal(accountId))
.sort(ID + " DESC").limit(1).exec(db);
if (rs.next()) ta = Transaction.of(rs);
rs.close();
}
if (ta != null){
var tags = ta.tags();
rs = select(TAG).from(TABLE_TAGS_TRANSACTIONS).leftJoin(TAG_ID,TABLE_TAGS,ID).where(TRANSACTION_ID,equal(ta.id())).exec(db);
while (rs.next()) tags.add(rs.getString(1));
rs.close();
}
return nullable(ta);
} catch (SQLException e) {
throw failedToSearchDb(t(Text.ACCOUNTING));
+1 -1
View File
@@ -41,7 +41,7 @@ subprojects {
testImplementation(platform("org.junit:junit-bom:5.10.0"))
testImplementation("org.junit.jupiter:junit-jupiter")
implementation("de.srsoftware:configuration.api:1.0.2")
implementation("de.srsoftware:tools.jdbc:2.0.8")
implementation("de.srsoftware:tools.jdbc:2.0.7")
implementation("de.srsoftware:tools.http:6.0.5")
implementation("de.srsoftware:tools.mime:1.1.4")
implementation("de.srsoftware:tools.logging:1.3.2")
@@ -67,16 +67,9 @@ public class SqliteDb extends BaseDb implements ContactDb{
db.setAutoCommit(false);
Query.delete().from(TABLE_CONTACTS).where(ID,equal(contact.id())).execute(db);
Query.delete().from(TABLE_CONTACTS_USERS).where(CONTACT_ID,equal(contact.id())).execute(db);
db.setAutoCommit(true);
} catch (SQLException e){
try {
db.rollback();
} catch (SQLException ignored){};
throw failedToDropObject(t(CONTACT_WITH_ID, ID,contact.id())).causedBy(e);
} finally {
try {
db.setAutoCommit(true);
} catch (SQLException ignored){};
}
}
@@ -69,15 +69,9 @@ CREATE TABLE IF NOT EXISTS {0} ( {1} VARCHAR(255) PRIMARY KEY, {2} VARCHAR(255)
update(table).set(STATUS).where(STATUS,equal(20)).prepare(db).apply(40).execute();
update(table).set(STATUS).where(STATUS,equal(10)).prepare(db).apply(20).execute();
update(table).set(STATUS).where(STATUS,equal(0)).prepare(db).apply(10).execute();
db.setAutoCommit(true);
} catch (SQLException e) {
try {
db.rollback();
} catch (SQLException ignored) {}
throw new RuntimeException(e);
} finally {
try {
db.setAutoCommit(true);
} catch (SQLException ignored){};
}
}
}
@@ -3,13 +3,11 @@ package de.srsoftware.umbrella.core.api;
import de.srsoftware.umbrella.core.exceptions.UmbrellaException;
import de.srsoftware.umbrella.core.model.Task;
import de.srsoftware.umbrella.core.model.UmbrellaUser;
import java.util.Collection;
import java.util.List;
import java.util.Map;
public interface TaskService {
void clonePermissions(long project_id, UmbrellaUser source, UmbrellaUser dest);
Map<Long, Task> listCompanyTasks(long companyId) throws UmbrellaException;
Map<Long, Task> listProjectTasks(long projectId) throws UmbrellaException;
Map<Long, Task> load(Collection<Long> taskIds);
@@ -77,11 +77,10 @@ public class Field {
public static final String HEAD = "head";
public static final String HOURS = "hours";
public static final String ID = "id";
public static final String INHERIT_FROM = "inherit_from";
public static final String INSTANTLY = "instantly";
public static final String ITEM = "item";
public static final String ITEM_CODE = "item_code";
public static final String ID = "id";
public static final String INSTANTLY = "instantly";
public static final String ITEM = "item";
public static final String ITEM_CODE = "item_code";
public static final String KEY = "key";
@@ -42,8 +42,8 @@ public class Path {
public static final String PROPERTY = "property";
public static final String PURPOSES = "purposes";
public static final String READ = "read";
public static final String REDIRECT = "redirect";
public static final String READ = "read";
public static final String REDIRECT = "redirect";
public static final String SEARCH = "search";
public static final String SELECT = "select";
+1
View File
@@ -0,0 +1 @@
*.db-journal
Binary file not shown.
Binary file not shown.
Binary file not shown.
+74
View File
@@ -0,0 +1,74 @@
{
"umbrella": {
"base_url": "http://127.0.0.1:5173",
"logging": {
"rootLevel": "INFO"
},
"http": {
"port": 8080
},
"threads": 16,
"modules": {
"accounting": {
"database": "demodata/accounting.db"
},
"bookmark": {
"database": "demodata/bookmark.db"
},
"company": {
"database": "demodata/company.db"
},
"contact": {
"database": "demodata/contacts.db"
},
"document": {
"database": "demodata/documents.db",
"templates": "demodata/templates"
},
"files": {
"database": "demodata/files.db",
"base_dir": "demodata/filestore"
},
"journal": {
"database": "demodata/journal.db"
},
"message": {
"database": "demodata/message.db",
"smtp": {
"from": "umbrella@example.com",
"host": "none",
"pass": "none",
"port": 587,
"user": "none"
}
},
"notes": {
"database": "demodata/notes.db"
},
"poll": {
"database": "demodata/poll.db"
},
"project": {
"database": "demodata/projects.db"
},
"stock": {
"database": "demodata/stock.db"
},
"tags": {
"database": "demodata/tags.db"
},
"task": {
"database": "demodata/tasks.db"
},
"time": {
"database": "demodata/times.db"
},
"user": {
"database": "demodata/users.db"
},
"wiki": {
"database": "demodata/wiki.db"
}
}
}
}
Binary file not shown.
Binary file not shown.
BIN
View File
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
@@ -198,17 +198,11 @@ CREATE TABLE IF NOT EXISTS {0} (
rs.close();
delete().from(TABLE_POSITIONS).where(DOCUMENT_ID,equal(docId)).execute(db);
delete().from(TABLE_DOCUMENTS).where(ID,equal(docId)).execute(db);
db.setAutoCommit(true);
if (number != null) return number;
throw failedToDropObject(t(DOCUMENT_WITH_ID, ID,docId));
} catch (SQLException e){
try {
db.rollback();
} catch (SQLException ignored){};
throw failedToDropObject(t(DOCUMENT_WITH_ID, ID,docId)).causedBy(e);
} finally {
try {
db.setAutoCommit(true);
} catch (SQLException ignored){};
}
}
@@ -223,16 +217,10 @@ CREATE TABLE IF NOT EXISTS {0} (
stmt.setLong(2,pos);
stmt.execute();
stmt.close();
db.setAutoCommit(true);
return pos;
} catch (SQLException e) {
try {
db.rollback();
} catch (SQLException ignored){};
throw failedToDropObjectFromObject(POSITION,pos,t(Text.DOCUMENT),docId).causedBy(e);
} finally {
try {
db.setAutoCommit(true);
} catch (SQLException ignored){};
}
}
@@ -594,15 +582,9 @@ CREATE TABLE IF NOT EXISTS {0} (
update(TABLE_POSITIONS).set(POS).where(DOCUMENT_ID,equal(docId)).where(POS,equal(pair.left())).prepare(db).apply(-pair.right()).close();
update(TABLE_POSITIONS).set(POS).where(DOCUMENT_ID,equal(docId)).where(POS,equal(pair.right())).prepare(db).apply(pair.left()).close();
update(TABLE_POSITIONS).set(POS).where(DOCUMENT_ID,equal(docId)).where(POS,equal(-pair.right())).prepare(db).apply(pair.right()).close();
db.setAutoCommit(true);
} catch (SQLException e) {
try {
db.rollback();
} catch (SQLException ignored){};
throw databaseException(FAILED_TO_SWITCH_POSITIONS,"a",pair.left(),"b",pair.right(),docId).causedBy(e);
} finally {
try {
db.setAutoCommit(true);
} catch (SQLException ignored){};
}
return pair;
}
-1
View File
@@ -122,7 +122,6 @@
<Route path="/stock/location/:location_id" component={Stock} />
<Route path="/stock/:item_id/view" component={Stock} />
<Route path="/stock/:owner/:owner_id/item/:owner_number" component={Stock} />
<Route path="/stock/:owner/:owner_id/item/:owner_number/view" component={Stock} />
<Route path="/tags" component={TagList} />
<Route path="/tags/use/:tag" component={TagUses} />
<Route path="/task" component={TaskList} />
+2 -3
View File
@@ -4,7 +4,6 @@
let {
enabled = true,
id = null,
autofocus = false,
getCandidates = dummyGetCandidates,
@@ -116,7 +115,7 @@
function select(index){
candidate = candidates[index];
disableDropdown();
<disableDropdown></disableDropdown>();
onSelect(candidate);
}
@@ -149,7 +148,7 @@
</style>
<span class="autocomplete">
<input type="text" bind:value={candidate.display} {onkeyup} autofocus={autofocus} {id} {onblur} disabled={!enabled} />
<input type="text" bind:value={candidate.display} {onkeyup} autofocus={autofocus} {id} {onblur} />
{#if candidates && candidates.length > 0}
<ul bind:this={list_elem} class="suggestions" tabindex="-1">
{#each candidates as candidate,i}
@@ -10,7 +10,6 @@
let {
addMember = (entry) => console.log(`no handler for addMember(${entry})`),
enabled = true,
dropMember = (member) => console.log(`no handler for dropMember(${member})`),
getCandidates = defaultGetCandidates,
members,
@@ -62,7 +61,7 @@
<tr>
<td>{t('add_object',{object:t('member')})}</td>
<td>
<Autocomplete {getCandidates} onSelect={addMember} {enabled} />
<Autocomplete {getCandidates} onSelect={addMember} />
</td>
</tr>
</tbody>
+4 -36
View File
@@ -4,47 +4,12 @@
import { api } from '../urls.svelte';
import { t } from '../translations.svelte';
import { error, yikes } from '../warn.svelte';
import { msToTime, now } from '../time.svelte';
import { timetrack } from '../user.svelte';
let interval = null;
const router = useTinyRouter();
async function addTime(task_id){
const url = api(`time/track_task/${task_id}`);
const resp = await fetch(url,{
credentials : 'include',
method : 'POST',
body : now()
}); // create new time or return time with assigned tasks
if (resp.ok) {
const track = await resp.json();
timetrack.running = track;
yikes();
} else {
error(resp);
}
}
function go(){
router.navigate('/time');
}
function ondragover(ev){
ev.preventDefault();
}
function ondragleave(ev){
ev.preventDefault();
}
function ondrop(ev){
let task_id = ev.dataTransfer.getData('task');
if (task_id) addTime(task_id);
}
async function stopTrack(){
if (timetrack.running.id){
const url = api(`time/${timetrack.running.id}/stop`);
@@ -63,6 +28,9 @@
}
}
function go(){
router.navigate('/time');
}
function updateElapsed(){
timetrack.elapsed = msToTime(Date.now() - timetrack.start);
@@ -86,7 +54,7 @@
{#if timetrack.running}
<span class="timetracking" {ondrop} {ondragover} {ondragleave} >
<span class="timetracking">
<span onclick={go} >{timetrack.running.subject} {#if timetrack.elapsed}({timetrack.elapsed}){/if}</span>
<button onclick={stopTrack} title={t('stop')} class="symbol"></button>
</span>
+1 -2
View File
@@ -6,7 +6,6 @@
import Autocomplete from './Autocomplete.svelte';
let {
enabled = true,
getCandidates = async text => {},
heading = t('add_object',{object:t('user')}),
users = $bindable({})
@@ -38,7 +37,7 @@
<tr>
<td>{heading}</td>
<td>
<Autocomplete {getCandidates} {onSelect} {enabled} />
<Autocomplete {getCandidates} {onSelect} />
</td>
</tr>
</tbody>
@@ -4,7 +4,7 @@
import { error, yikes } from '../../warn.svelte';
import { t } from '../../translations.svelte';
import EntryForm from './add_entry.svelte';
import EntryForm from './add_entry_new.svelte';
import Transaction from './transaction.svelte';
let { id } = $props();
@@ -173,5 +173,5 @@
</table>
</fieldset>
<EntryForm {account} {onSave} />
<EntryForm {account} {onSave} {users} />
{/if}
@@ -0,0 +1,205 @@
<script>
import { useTinyRouter } from 'svelte-tiny-router';
import { t } from '../../translations.svelte';
import { api, post } from '../../urls.svelte';
import { error, yikes } from '../../warn.svelte';
import { user } from '../../user.svelte';
import Autocomplete from '../../Components/Autocomplete.svelte';
import Tags from '../tags/TagList.svelte';
let defaultAccount = {
id : 0,
name : '',
currency : ''
};
let { account = defaultAccount, new_account = false, onSave = () => {}, users } = $props();
let entry = $state({
account,
date : new Date().toISOString().substring(0, 10),
source : {
display: user.name,
id: user.id
},
destination : {},
amount : 0.0,
purpose : {},
tags : []
});
let router = useTinyRouter();
async function dst_selected(destination){
destination = JSON.parse(JSON.stringify(destination));
let source = JSON.parse(JSON.stringify(entry.source));
const url = api(`accounting/${entry.account.id}/tags`)
const res = await post(url,{source,destination});
if (res.ok) {
yikes();
const json = await res.json();
await proposePurpose();
entry.tags = json;
} else error(res);
}
function focusOnEnter(ev,id){
if (ev.key == 'Enter') {
proposePurpose();
document.getElementById(id).focus();
}
}
async function getAccountTags(text){
if (!text) return [];
const url = api(`accounting/${entry.account.id}/tags`)
return await getProposals(text,url);
}
async function getDestinations(text){
const url = api('accounting/destinations');
return await getProposals(text,url);
}
async function getProposals(text,url){
const res = await post(url,text);
if (res.ok){
yikes();
const input = await res.json();
return Object.values(input).map(mapDisplay);
} else {
error(res);
return {};
}
}
async function getPurposes(text) {
const url = api('accounting/purposes');
return await getProposals(text,url);
}
async function getSources(text){
const url = api('accounting/sources');
return await getProposals(text,url);
}
function gotoTags(purpose){
document.getElementById('new_tag_input');
}
function mapDisplay(object){
if (object.display){
return object;
} else if (object.name) {
return {...object, display: object.name};
} else {
return { display : object }
}
}
async function proposePurpose(){
console.log('proposePurpose()');
const amount = entry.amount;
const source = entry.source;
const destination = entry.destination;
const url = api(`accounting/${account.id}/purposes`);
const res = await post(url,{source,destination,amount});
if (res.ok) {
yikes();
var lastTransaction = await res.json();
console.log({lastTransaction,users:JSON.parse(JSON.stringify(users))});
entry.purpose = { display: lastTransaction.purpose};
entry.tags = lastTransaction.tags;
if (lastTransaction.source.value){
if (users[lastTransaction.source.value]){
let user = users[lastTransaction.source.value];
entry.source = { id : +lastTransaction.source.value, display : user.name };
} else entry.source = { display: lastTransaction.source.value };
}
if (lastTransaction.destination.value){
if (users[lastTransaction.destination.value]){
let user = users[lastTransaction.destination.value];
entry.destination = { id : +lastTransaction.destination.value, display : user.name };
} else entry.destination = { display: lastTransaction.destination.value };
}
} else error(res);
}
async function save(){
let data = {
...entry,
purpose: entry.purpose.display
}
let url = api('accounting');
let res = await post(url, data);
if (res.ok) {
yikes();
if (new_account){
router.navigate('/accounting');
return;
}
//entry.tags = [];
onSave();
document.getElementById('date-input').focus();
} else error(res);
}
</script>
<style>
hr{
grid-column: 1 / -1;
margin: 0.5rem 0;
border: 0;
height: 1px;
align-self: center;
background: red;
}
</style>
<fieldset class="grid2 new_transaction">
{#if new_account}
<legend>{t('create_new_object',{object:t('account')})}</legend>
<span style="display:none"></span>
<span>{t('account name')}</span>
<span>
<input type="text" bind:value={entry.account.name} />
</span>
<span>{t('currency')}</span>
<span>
<input type="text" bind:value={entry.account.currency} />
</span>
<hr/>
<span style="grid-column-end: span 2">{t('first transaction')}</span>
{:else}
<legend>{t('add_object',{object:t('transaction')})}</legend>
<span style="display:none"></span>
{/if}
<span>{t('date')}</span>
<span>
<input type="date" bind:value={entry.date} id="date-input" />
</span>
<span>{t('amount')}</span>
<span>
<input type="number" bind:value={entry.amount} onkeyup={e => focusOnEnter(e,'source-input')} />&nbsp;{entry.account.currency}
</span>
<span>{t('source')}</span>
<Autocomplete bind:candidate={entry.source} getCandidates={getSources} id="source-input" />
<span>{t('destination')}</span>
<Autocomplete bind:candidate={entry.destination} getCandidates={getDestinations} onSelect={dst_selected} />
<span>{t('purpose')}</span>
<Autocomplete bind:candidate={entry.purpose} getCandidates={getPurposes} onCommit={gotoTags} id="purpose_input" />
<span>{t('tags')}</span>
<Tags getCandidates={getAccountTags} module={null} bind:tags={entry.tags} onEmptyCommit={save} />
<span></span>
<span>
<button onclick={save}>{t('save')}</button>
</span>
</fieldset>
+1 -7
View File
@@ -1,7 +1,6 @@
<script>
import { onMount } from 'svelte';
import { useTinyRouter } from 'svelte-tiny-router';
import { t } from '../../translations.svelte';
import LineEditor from '../../Components/LineEditor.svelte';
import MarkdownEditor from '../../Components/MarkdownEditor.svelte';
@@ -24,17 +23,13 @@
function movedown(){
movePos(pos.number,1);
}
async function toggleOptional(ev){
const ok = await submit(`${prefix}.optional`,!pos.optional);
if (ok) pos.optional = !pos.optional;
}
</script>
<style>
.move{
vertical-align: middle;
}
tr > *:nth-child(1){
text-align: right;
}
@@ -71,7 +66,6 @@
<span onclick={moveup}>⏫</span>
{/if}
<span onclick={() => drop(pos.number)}>❌</span>
<span class="symbol" onclick={toggleOptional} title={t(pos.optional?'optional position':'fixed position')}>{pos.optional?'':''}</span>
<span onclick={movedown}>⏬</span>
{/if}
</td>
+1 -6
View File
@@ -207,11 +207,6 @@
return false;
}
function ondragstart(ev, task){
dragged = task;
ev.dataTransfer.setData("task",task.id);
}
function openTask(task_id){
window.open(`/task/${task_id}/view`, '_blank').focus();
}
@@ -311,7 +306,7 @@
<div class={['state_'+state, is_custom(state) ? '':'state_custom' ,highlight.user == u.id && highlight.state == state ? 'highlight':'']} ondragover={ev => hover(ev,u.id,state)} ondragleave={e => delete highlight.user} ondrop={ev => drop(u.id,state)} >
{#each Object.values(tasks[u.id][state]).sort(byName) as task}
{#if !filter || task.name.toLowerCase().includes(filter) || (task.tags && task.tags.filter(tag => tag.toLowerCase().includes(filter)).length)}
<Card onclick={e => openTask(task.id)} ondragstart={ev => ondragstart(ev,task)} {task} tag_colors={project.tag_colors} />
<Card onclick={e => openTask(task.id)} ondragstart={ev => dragged=task} {task} tag_colors={project.tag_colors} />
{/if}
{/each}
<div class="add_task" onclick={ev => show_task_form(project.id,u.id,+state)}>
+2 -12
View File
@@ -25,13 +25,12 @@
let tasks = $state(null);
let show_closed = $state(false);
let new_color = $state({tag:null,color:'#00aa00'})
let inherit_from = $state(0);
let new_state = $state({code:null,name:null})
let state_available=$derived(new_state.name && new_state.code && !project.allowed_states[new_state.code]);
async function addMember(user){
return await update({new_member:+user.id,inherit_from});
return await update({new_member:+user.id});
}
async function addState(){
@@ -240,16 +239,7 @@
</label>
<div class="em">{t('members')}</div>
<div class="em">
<PermissionEditor members={project.members} {updatePermission} {addMember} {dropMember} enabled={inherit_from != 0}/>
<label>
└→ Inherit task permissions from:
<select bind:value={inherit_from}>
<option value={0}>{t('select user')}</option>
{#each Object.values(project.members) as member (member.user.id)}
<option value={member.user.id}>{member.user.name}</option>
{/each}
</select>
</label>
<PermissionEditor members={project.members} {updatePermission} {addMember} {dropMember} />
</div>
{#if project.allowed_states}
{#each Object.keys(project.allowed_states) as key,idx}
+1 -15
View File
@@ -265,10 +265,8 @@
<legend>
{t('stock')}
</legend>
{#if Object.keys(stock.items).length }
{t('items')}
<ul>
{#each Object.values(stock.items) as item}
{#each Object.values(stock) as item}
<li>
<a href="/stock/{item.owner.type}/{item.owner.id}/item/{item.owner_number}" {onclick} >
{item.name} [{t('code')}: <span class="code">{item.code}</span>]
@@ -277,18 +275,6 @@
</li>
{/each}
</ul>
{/if}
{#if Object.keys(stock.locations).length}
{t('locations')}
<ul>
{#each Object.values(stock.locations) as loc (loc.id)}
<li>
<a href="/stock/location/{loc.id}">{loc.name}</a>
<br/> {@html loc.description.rendered}
</li>
{/each}
</ul>
{/if}
</fieldset>
{/if}
{#if times}
+1 -1
View File
@@ -102,7 +102,7 @@
<ul>
{#each locations as location}
<li onclick={e => toggleChildren(e, location)}
class="{location.locations?'expanded':'collapsed'} {location.highlight?'highlight':null} {selected && selected.id == location.id?'selected':null}"
class="{location.locations?'expanded':'collapsed'} {location.highlight?'highlight':null}"
draggable={true}
ondragover={e => drag_over(e,location)}
ondrop={e => onDrop(e,location)}
+3 -38
View File
@@ -81,10 +81,6 @@
router.navigate(`/project/${project.id}/view`)
}
function gotoTask(tid){
if (id) router.navigate(`/task/${tid}/view`)
}
function handleUpdateEvent(evt){
let json = JSON.parse(evt.data);
if (json.task) {
@@ -123,8 +119,7 @@
if (task.show_closed) show_closed = true;
loadChildren();
if (task.project_id) loadProject();
if (task.parent_task_id) await loadParent();
loadSiblings();
if (task.parent_task_id) loadParent();
} else error(resp);
}
@@ -134,36 +129,13 @@
if (resp.ok){
project = await resp.json();
yikes();
} else error(resp);
}
async function loadSiblings(){
const url = api(`task/list`);
const select = task.parent_task_id ? {parent_task_id:task.parent_task_id} : {project_id: task.project_id};
select.show_closed = false;
const res = await post(url,select);
if (res.ok){
task.siblings = await res.json();
let last = null;
let before = undefined;
let after = null;
for (let [sid, t] of Object.entries(task.siblings).toSorted((a,b) => a[1].name.localeCompare(b[1].name))){
if (before !== undefined) {
after = +sid;
break;
}
if (id == sid) before = last;
last = +sid;
}
task.before = before;
task.after = after;
} else error(res)
} else error(await resp.text());
}
function parentClick(ev){
ev.preventDefault();
if (!task.parent_task_id) return;
gotoTask(task.parent_task_id);
router.navigate(`/task/${task.parent_task_id}/view`);
return false;
}
@@ -260,17 +232,10 @@
{/if}
<button class="symbol" title={t('edit')} onclick={parentRightClick}></button>
{/if}
</div>
<div>{t('task')}</div>
<div class="name">
<LineEditor bind:value={task.name} editable={true} onSet={val => update({name:val})} />
{#if task.before}
<button class="symbol" title={task.siblings[task.before].name} onclick={e => gotoTask(task.before)}></button>
{/if}
{#if task.after}
<button class="symbol" title={task.siblings[task.after].name} onclick={e => gotoTask(task.after)}></button>
{/if}
<button class="symbol" title={t('settings')} onclick={toggleSettings}></button>
<button class="symbol" title={t('timetracking')} onclick={addTime}></button>
</div>
+1 -1
View File
@@ -21,4 +21,4 @@ export function t(key,args = {}){
}
for (var key of Object.keys(args)) set = set.replace(`{${key}}`,args[key]);
return set;
}
}
@@ -22,7 +22,6 @@ import static de.srsoftware.umbrella.messagebus.MessageBus.messageBus;
import static de.srsoftware.umbrella.messagebus.events.Event.EventType.CREATE;
import static de.srsoftware.umbrella.project.Constants.CONFIG_DATABASE;
import static java.lang.Boolean.TRUE;
import static java.lang.System.Logger.Level.WARNING;
import static java.net.HttpURLConnection.HTTP_BAD_REQUEST;
import com.sun.net.httpserver.HttpExchange;
@@ -139,7 +138,6 @@ public class ProjectModule extends BaseHandler implements ProjectService {
private void dropMember(Project project, long userId) {
if (project.members().get(userId).permission() == OWNER) throw forbidden("You may not remove the owner of the project");
projectDb.dropMember(project.id(),userId);
LOG.log(WARNING,"Removing member from project tasks not implemented"); // TODO
project.members().remove(userId);
}
@@ -225,16 +223,7 @@ public class ProjectModule extends BaseHandler implements ProjectService {
UmbrellaUser newMember = null;
if (json.has(DROP_MEMBER) && json.get(DROP_MEMBER) instanceof Number id) dropMember(project,id.longValue());
if (json.has(MEMBERS) && json.get(MEMBERS) instanceof JSONObject memberJson) patchMembers(project,memberJson);
if (json.has(NEW_MEMBER) && json.get(NEW_MEMBER) instanceof Number num) {
UmbrellaUser inheritFrom = null;
if (json.has(INHERIT_FROM)){
if (!(json.get(INHERIT_FROM) instanceof Number uid)) throw invalidField(INHERIT_FROM,Text.NUMBER);
inheritFrom = userService().loadUser(uid.longValue());
if (!project.hasMember(inheritFrom)) throw notAmember(inheritFrom);
}
newMember = addMember(project,num.longValue());
if (inheritFrom != null) taskService().clonePermissions(project.id(),inheritFrom,newMember);
}
if (json.has(NEW_MEMBER) && json.get(NEW_MEMBER) instanceof Number num) newMember = addMember(project,num.longValue());
project = projectDb.save(project.patch(json), user);
messageBus().dispatch(newMember != null ? new ProjectEvent(user,project,newMember) : new ProjectEvent(user,project, old));
@@ -63,16 +63,10 @@ public class SqliteDb extends BaseDb implements StockDb {
rs.close();
if (propertyId == null || propertyId == 0) throw failedToStoreObject(t(PROPERTY));
insertInto(TABLE_ITEM_PROPERTIES,ITEM_ID,PROPERTY_ID,VALUE).values(itemId,propertyId,value).execute(db).close();
db.setAutoCommit(true);
return new Property(propertyId,name,value,unit);
} catch (SQLException e) {
try {
db.rollback();
} catch (SQLException ignored){};
throw failedToStoreObject(t(PROPERTY)).causedBy(e);
} finally {
try {
db.setAutoCommit(true);
} catch (SQLException ignored){};
}
}
@@ -132,7 +126,7 @@ public class SqliteDb extends BaseDb implements StockDb {
private void createItemsTable() {
try {
var sql = "CREATE TABLE IF NOT EXISTS {0} ( {1} VARCHAR(255) PRIMARY KEY, {2} VARCHAR(255) NOT NULL, {3} TEXT, {4} VARCHAR(255))";
sql = format(sql, TABLE_ITEMS, ID, Field.CODE, NAME, LOCATION_ID);
sql = format(sql, TABLE_ITEMS, ID, Field.CODE, NAME, LOCATION_ID);
db.prepareStatement(sql).execute();
} catch (SQLException e) {
throw failedToCreateTable(TABLE_ITEMS).causedBy(e);
@@ -142,7 +136,7 @@ public class SqliteDb extends BaseDb implements StockDb {
private void createItemPropsTable() {
try {
var sql = "CREATE TABLE IF NOT EXISTS {0} ( {1} INT NOT NULL, {2} INT NOT NULL, {3} VARCHAR(255) NOT NULL, PRIMARY KEY({1}, {2}))";
sql = format(sql, TABLE_ITEM_PROPERTIES, ITEM_ID, PROPERTY_ID,VALUE);
sql = format(sql, TABLE_ITEM_PROPERTIES, ITEM_ID, PROPERTY_ID,VALUE);
db.prepareStatement(sql).execute();
} catch (SQLException e) {
throw failedToCreateTable(TABLE_ITEM_PROPERTIES).causedBy(e);
@@ -152,7 +146,7 @@ public class SqliteDb extends BaseDb implements StockDb {
private void createLocationsTable() {
try {
var sql = "CREATE TABLE IF NOT EXISTS {0} ( {1} VARCHAR(255) PRIMARY KEY, {2} VARCHAR(255) DEFAULT NULL, {3} VARCHAR(255) NOT NULL, {4} TEXT)";
sql = format(sql, TABLE_LOCATIONS, ID, LOCATION_ID, NAME, DESCRIPTION);
sql = format(sql, TABLE_LOCATIONS, ID, LOCATION_ID, NAME, DESCRIPTION);
db.prepareStatement(sql).execute();
} catch (SQLException e) {
throw failedToCreateTable(TABLE_LOCATIONS).causedBy(e);
@@ -162,7 +156,7 @@ public class SqliteDb extends BaseDb implements StockDb {
private void createPropertiesTable() {
try {
var sql = "CREATE TABLE IF NOT EXISTS {0} ( {1} INTEGER PRIMARY KEY, {2} VARCHAR(255) NOT NULL, {3} INT NOT NULL, {4} VARCHAR(255))";
sql = format(sql, TABLE_PROPERTIES, ID, NAME, TYPE, UNIT);
sql = format(sql, TABLE_PROPERTIES, ID, NAME, TYPE, UNIT);
db.prepareStatement(sql).execute();
} catch (SQLException e) {
throw failedToCreateTable(TABLE_PROPERTIES).causedBy(e);
@@ -207,18 +201,18 @@ public class SqliteDb extends BaseDb implements StockDb {
}
@Override
public Map<Long, Item> findItems(Collection<Owner> owners, Collection<String> keys, boolean fulltext) {
public Map<Long, Item> find(Collection<Owner> owners, Collection<String> keys, boolean fulltext) {
try {
var items = new HashMap<Long,Item>();
var ownerCodes = owners.stream().map(Owner::dbCode).toArray();
var query = select(ALL).from(TABLE_ITEMS).where(OWNER, in(ownerCodes));
var query = select(ALL).from(TABLE_ITEMS).where(OWNER, in(ownerCodes));
if (fulltext) {
query.leftJoin(ID,TABLE_ITEM_PROPERTIES,ITEM_ID);
for (var key : keys) query.where(format("CONCAT({0},\" \",{1},\" \",{2})", NAME, DESCRIPTION,VALUE),like("%"+key+"%"));
} else {
for (var key : keys) query.where(NAME,like("%"+key+"%"));
}
var rs = query.exec(db);
var items = new HashMap<Long,Item>();
var rs = query.exec(db);
while (rs.next()){
var item = Item.of(rs);
items.put(item.id(),item);
@@ -230,33 +224,10 @@ public class SqliteDb extends BaseDb implements StockDb {
}
}
@Override
public Map<Long, Location> findLocations(Collection<Owner> owners, Collection<String> keys, boolean fulltext) {
try {
var ownerCodes = owners.stream().map(Owner::dbCode).toArray();
var query = select(ALL).from(TABLE_LOCATIONS).where(OWNER, in(ownerCodes));
if (fulltext) {
for (var key : keys) query.where(format("CONCAT({0},\" \",{1})", NAME, DESCRIPTION), like("%" + key + "%"));
} else {
for (var key : keys) query.where(NAME, like("%" + key + "%"));
}
var rs = query.exec(db);
var locations = new HashMap<Long, Location>();
while (rs.next()) {
var location = DbLocation.of(rs);
locations.put(location.id(), location);
}
rs.close();
return locations;
} catch (SQLException e){
throw databaseException(FAILED_TO_LIST_ENTITIES, TYPE,t(LOCATIONS)).causedBy(e);
}
}
@Override
public Collection<DbLocation> listChildLocations(long parentId) {
try {
var rs = select(ALL).from(TABLE_LOCATIONS).where(PARENT_LOCATION_ID,equal(parentId)).exec(db);
var rs = select(ALL).from(TABLE_LOCATIONS).where(PARENT_LOCATION_ID,equal(parentId)).exec(db);
var list = new ArrayList<DbLocation>();
while (rs.next()) list.add(DbLocation.of(rs));
rs.close();
@@ -269,7 +240,7 @@ public class SqliteDb extends BaseDb implements StockDb {
@Override
public Collection<DbLocation> listCompanyLocations(Company company) {
try {
var rs = select(ALL).from(TABLE_LOCATIONS).where(OWNER,equal(company.dbCode())).where(PARENT_LOCATION_ID,isNull()).exec(db);
var rs = select(ALL).from(TABLE_LOCATIONS).where(OWNER,equal(company.dbCode())).where(PARENT_LOCATION_ID,isNull()).exec(db);
var list = new ArrayList<DbLocation>();
while (rs.next()) list.add(DbLocation.of(rs));
rs.close();
@@ -630,16 +601,13 @@ public class SqliteDb extends BaseDb implements StockDb {
replaceLocationsTable();
replaceItemsTable();
replaceItemPropsTable();
db.setAutoCommit(true);
} catch (Exception e) {
try {
db.rollback();
} catch (SQLException ignored) {
}
throw databaseException(FAILED_TO_UPDATE_OBJECT, OBJECT, t(TABLE_WITH_NAME,NAME,TABLE_LOCATIONS)).causedBy(e);
} finally {
try {
db.setAutoCommit(true);
} catch (SQLException ignored){};
}
}
@@ -10,8 +10,7 @@ import java.util.Map;
public interface StockDb {
Property addNewProperty(long itemId, String name, Object value, String unit);
Location delete(DbLocation location);
Map<Long, Item> findItems(Collection<Owner> owners, Collection<String> keys, boolean fulltext);
Map<Long, Location> findLocations(Collection<Owner> owners, Collection<String> keys, boolean fulltext);
Map<Long, Item> find(Collection<Owner> owners, Collection<String> keys, boolean fulltext);
Collection<DbLocation> listChildLocations(long parentId);
Collection<DbLocation> listCompanyLocations(Company company);
Collection<Item> listItemsAt(Location location);
@@ -459,13 +459,12 @@ public class StockModule extends BaseHandler implements StockService {
private boolean postSearch(UmbrellaUser user, HttpExchange ex) throws IOException {
var json = json(ex);
if (!(json.has(KEY) && json.get(KEY) instanceof String key)) throw missingField(KEY);
var keys = Arrays.asList(key.split(" "));
var fulltext = json.has(FULLTEXT) && json.get(FULLTEXT) instanceof Boolean val && val;
var keys = Arrays.asList(key.split(" "));
var fulltext = json.has(FULLTEXT) && json.get(FULLTEXT) instanceof Boolean val && val;
Set<Owner> owners = new HashSet<>(companyService().listCompaniesOf(user).values());
owners.add(user);
var items = stockDb.findItems(owners,keys,fulltext);
var locations = stockDb.findLocations(owners,keys,fulltext);
return sendContent(ex,Map.of("items",mapValues(items),"locations",mapValues(locations)));
var items = stockDb.find(owners,keys,fulltext);
return sendContent(ex,mapValues(items));
}
@Override
@@ -260,7 +260,7 @@ CREATE TABLE IF NOT EXISTS {0} (
try {
var query = select(ALL).from(TABLE_TASKS).leftJoin(ID,TABLE_TASKS_USERS,TASK_ID).where(USER_ID,equal(userId));
if (!showClosed) query.where(STATUS,lessThan(COMPLETE.code()));
var rs = query.sort("(CASE due_date WHEN \"\" THEN '9999-99-99' ELSE IFNULL(due_date,'9999-99-99') END), status COLLATE NOCASE").limit(limit == null ? -1 : limit).skip(offset).exec(db);
var rs = query.sort("(CASE due_date WHEN \"\" THEN '9999-99-99' ELSE IFNULL(due_date,'9999-99-99') END), status COLLATE NOCASE").limit(limit).skip(offset).exec(db);
var map = new ArrayList<Task>();
while (rs.next()) map.add(Task.of(rs));
rs.close();
@@ -340,6 +340,7 @@ CREATE TABLE IF NOT EXISTS {0} (
var addQuery = replaceInto(TABLE_TASK_DEPENDENCIES, TASK_ID, REQUIRED_TASK_ID);
for (var reqId : task.requiredTasksIds()) addQuery.values(task.id(), reqId);
addQuery.execute(db).close();
db.setAutoCommit(true);
}
task.clean(REQUIRED_TASKS_IDS);
@@ -353,14 +354,7 @@ CREATE TABLE IF NOT EXISTS {0} (
}
return task;
} catch (SQLException e){
try {
db.rollback();
} catch (SQLException ignored){};
throw failedToStoreObject(task.name()).causedBy(e);
} finally {
try {
db.setAutoCommit(true);
} catch (SQLException ignored){};
}
}
@@ -66,28 +66,6 @@ public class TaskModule extends BaseHandler implements TaskService {
return user;
}
@Override
public void clonePermissions(long project_id, UmbrellaUser source, UmbrellaUser dest) {
var taskMap = taskDb.listProjectTasks(project_id,null,true);
loadMembers(taskMap.values());
for (var task : taskMap.values()) {
var memberShip = task.members().get(source.id());
Permission granted = memberShip == null ? EDIT : switch (memberShip.permission()) {
case ASSIGNEE,
EDIT,
OWNER ->
EDIT;
case READ_ONLY ->
READ_ONLY;
};
var newMember = new Member(dest, granted);
task.members().put(dest.id(), newMember);
task.dirty(MEMBERS);
taskDb.save(task);
}
}
private boolean deleteTask(HttpExchange ex, long taskId, UmbrellaUser user) throws IOException {
var task = loadMembers(taskDb.load(taskId));
var member = task.members().get(user.id());
@@ -84,16 +84,10 @@ CREATE TABLE IF NOT EXISTS {0} (
db.setAutoCommit(false);
Query.delete().from(TABLE_TASK_TIMES).where(TIME_ID,equal(timeId)).execute(db);
Query.delete().from(TABLE_TIMES).where(ID,equal(timeId)).execute(db);
db.setAutoCommit(false);
return timeId;
} catch (SQLException e) {
try {
db.rollback();
} catch (SQLException ignored) {}
throw failedToDropObject(t(TIME_WITH_ID, ID,timeId)).causedBy(e);
} finally {
try {
db.setAutoCommit(true);
} catch (SQLException ignored){};
}
}
-4
View File
@@ -167,7 +167,6 @@
"filter": "Filter",
"filter by tags": "Nach Tags filtern",
"first_transaction": "erste Transaktion",
"fixed position": "fester Posten",
"footer": "Fuß-Text",
"foreign_id": "externe Kennung",
"forgot_pass" : "Password vergessen?",
@@ -279,9 +278,7 @@
"oidc_Login" : "Anmeldung mit OIDC",
"old_password": "altes Passwort",
"offer": "Angebot",
"option": "Option",
"optional position": "optionaler Posten",
"options": "Optionen",
"organization": "Organisation",
"other party": "Gegenseite",
@@ -338,7 +335,6 @@
"select_customer": "Kunde auswählen",
"select_property": "Eigenschaft auswählen",
"select_state": "Status wählen",
"select user": "Nutzer auswählen",
"send_document": "Dokument versenden",
"sender": "Absender",
"sender_bank_account": "Bankverbindung",
+2 -6
View File
@@ -167,7 +167,6 @@
"filter": "filter",
"filter by tags": "filter by tags",
"first_transaction": "first transaction",
"fixed position": "fixed position",
"footer": "footer",
"foreign_id": "external ID",
"forgot_pass" : "forgot password?",
@@ -279,9 +278,7 @@
"oidc_Login" : "Login via OIDC",
"old_password": "old password",
"offer": "offer",
"option": "option",
"optional position": "optional position",
"options": "options",
"organization": "organization",
"other party": "other party",
@@ -332,13 +329,12 @@
"saved": "saved",
"save_object": "save {object}",
"search": "search",
"searching…": "searching…",
"searching…": "searhcing…",
"select a new parent for {entity}": "select a new parent for '{entity}'",
"select_company" : "select on of you companies:",
"select_customer": "select customer",
"select_property": "select property",
"select_state": "select state",
"select user": "select user",
"send_document": "send document",
"sender": "sender",
"sender_bank_account": "bank account",
@@ -383,7 +379,7 @@
"subtask": "subtask",
"subtasks": "subtasks",
"succeeding_document": "succeeding document",
"sum external":"sum of external positions",
"sum external";"sum of external positions",
"sum_of_records": "sum of records",
"sums": "sums",
@@ -341,18 +341,6 @@ tr:hover .taglist .tag button {
color: yellow;
}
.positions tr:nth-child(2n){
border-bottom-color: red;
}
.positions tbody tr:last-child{
border-top-color: red;
}
.locations .selected span{
background: brown;
color: yellow;
}
@media screen and (max-width: 900px) {
#app nav a{
background: black;
@@ -356,10 +356,6 @@ span.timetracking {
grid-column-end: span 2;
}
.markdown svg{
display: block;
}
.editable:hover{
border: 1px dotted;
}
@@ -390,7 +386,6 @@ span.timetracking {
table{
min-width: 30vw;
border-collapse: collapse;
}
.start_end{
@@ -486,14 +481,6 @@ select.autocomplete{
width: calc(100% - 50px);
}
.positions tr:nth-child(2n){
border-bottom: 1px solid;
}
.positions tbody tr:last-child{
border-top: 3px solid;
}
@media screen and (min-width: 900px) {
#app nav > button.symbol{
display: none;
@@ -26,11 +26,6 @@ input{
color: orange;
}
input:disabled{
border: 1px solid gray;
background: #444;
}
legend.date,
legend.time{
background-color: black;
@@ -337,18 +332,6 @@ code,
color: black;
}
.positions tr:nth-child(2n){
border-bottom-color: gold;
}
.positions tbody tr:last-child{
border-top-color: gold;
}
.locations .selected span{
background: gold;
color: black;
}
@media screen and (max-width: 900px) {
#app nav a{
background: black;
@@ -450,10 +450,6 @@ span.timetracking {
grid-column-end: span 2;
}
.markdown svg{
display: block;
}
.editable:hover{
border: 1px dotted;
}
@@ -484,7 +480,6 @@ span.timetracking {
table{
min-width: 30vw;
border-collapse: collapse;
}
.start_end{
@@ -590,14 +585,6 @@ select.autocomplete{
width: calc(100% - 50px);
}
.positions tr:nth-child(2n){
border-bottom: 1px solid;
}
.positions tbody tr:last-child{
border-top: 3px solid;
}
@media screen and (min-width: 900px) {
#app nav > button.symbol{
display: none;
@@ -288,18 +288,6 @@ tr:hover .taglist .tag button {
background: cyan;
}
.positions tr:nth-child(2n){
border-bottom-color: blue;
}
.positions tbody tr:last-child{
border-top-color: blue;
}
.locations .selected span{
background: #afffff;
color: black;
}
@media screen and (max-width: 900px) {
#app nav a{
background: white;
-13
View File
@@ -449,10 +449,6 @@ span.timetracking {
grid-column-end: span 2;
}
.markdown svg{
display: block;
}
.editable:hover{
border: 1px dotted;
}
@@ -483,7 +479,6 @@ span.timetracking {
table{
min-width: 30vw;
border-collapse: collapse;
}
.start_end{
@@ -579,14 +574,6 @@ select.autocomplete{
width: calc(100% - 50px);
}
.positions tr:nth-child(2n){
border-bottom: 1px solid;
}
.positions tbody tr:last-child{
border-top: 3px solid;
}
@media screen and (min-width: 900px) {
#app nav > button.symbol{
display: none;