first version

This commit is contained in:
2026-10-07 15:53:12 +02:00
commit 9d8c2a0488
14 changed files with 1481 additions and 0 deletions
+75
View File
@@ -0,0 +1,75 @@
// Minimal Kimai REST client (https://www.kimai.org/documentation/rest-api.html)
var Kimai = (() => {
class Client {
constructor({ kimaiUrl, authMode, kimaiUser, kimaiToken }) {
this.base = String(kimaiUrl || '').replace(/\/+$/, '');
this.authMode = authMode || 'bearer';
this.user = kimaiUser;
this.token = kimaiToken;
}
headers() {
const h = { Accept: 'application/json' };
if (this.authMode === 'bearer') h.Authorization = `Bearer ${this.token}`;
else { h['X-AUTH-USER'] = this.user; h['X-AUTH-TOKEN'] = this.token; }
return h;
}
async req(path, { method = 'GET', query, body } = {}) {
if (!this.base) throw new Error('Kimai URL not configured');
const url = new URL(this.base + path);
for (const [k, v] of Object.entries(query || {})) if (v != null) url.searchParams.set(k, v);
const res = await fetch(url, {
method,
headers: { ...this.headers(), ...(body ? { 'Content-Type': 'application/json' } : {}) },
body: body ? JSON.stringify(body) : undefined,
credentials: 'omit',
});
const text = await res.text();
let data = null;
try { data = text ? JSON.parse(text) : null; } catch { /* keep raw text */ }
if (!res.ok) {
const msg = data?.message || data?.error?.message || text.slice(0, 200) || res.statusText;
const details = data?.errors ? ' ' + JSON.stringify(data.errors) : '';
throw new Error(`Kimai ${res.status}: ${msg}${details}`);
}
return { data, res };
}
async all(path, query = {}) {
const out = [];
for (let page = 1; ; page++) {
const { data, res } = await this.req(path, { query: { ...query, page, size: 250 } });
out.push(...(data || []));
const pages = +res.headers.get('X-Total-Pages') || 1;
if (page >= pages || !data?.length) return out;
}
}
version() { return this.req('/api/version').then((r) => r.data); }
me() { return this.req('/api/users/me').then((r) => r.data); }
projects() { return this.req('/api/projects', { query: { visible: 1 } }).then((r) => r.data); }
activities() { return this.req('/api/activities', { query: { visible: 1 } }).then((r) => r.data); }
// Entries of the current user between two ISO dates (inclusive)
timesheets(fromIso, toIso) {
return this.all('/api/timesheets', { begin: `${fromIso}T00:00:00`, end: `${toIso}T23:59:59` });
}
createTimesheet(entry) {
return this.req('/api/timesheets', { method: 'POST', body: entry }).then((r) => r.data);
}
}
// Kimai returns "2026-10-07T08:00:00+0200" in the user's timezone: read wall-clock directly.
function toLocal(ts) {
if (!ts.begin || !ts.end) return null; // running timer
const date = ts.begin.slice(0, 10);
const start = U.toMin(ts.begin.slice(11, 16));
let end = U.toMin(ts.end.slice(11, 16));
if (ts.end.slice(0, 10) !== date) end = 24 * 60; // crosses midnight, clamp
return { id: ts.id, date, start, end, project: ts.project, activity: ts.activity, description: ts.description };
}
return { Client, toLocal };
})();