Refine Maxi task board UX

This commit is contained in:
Iris
2026-06-26 18:39:46 +00:00
parent fe840da278
commit 6be8de566e
4 changed files with 118 additions and 52 deletions
+50 -17
View File
@@ -8,8 +8,7 @@ const columns = [
const states = columns.map((column) => column.state);
const priorities = ["High", "Medium", "Low"];
const assignees = [
{ value: "bao", label: "Bao" },
{ value: "iris", label: "Iris" },
{ value: "luna", label: "Luna" },
{ value: "maxi", label: "Maxi" }
];
@@ -24,6 +23,7 @@ const state = {
selectedTaskId: null,
draggedTaskId: null,
dragOverKey: null,
toast: null,
error: ""
};
@@ -103,7 +103,7 @@ function normalizeTask(task) {
...task,
state: task.state === "In Progress" ? "In progress" : task.state,
priority: task.priority === "Normal" ? "Medium" : task.priority === "Urgent" ? "High" : task.priority,
assignee: task.assignee || "bao"
assignee: normalizeAssignee(task.assignee)
};
}
@@ -139,6 +139,7 @@ function renderApp() {
<section class="content">${state.view === "settings" ? settingsView() : boardView()}</section>
</main>
</section>
${toastView()}
${createModal()}
${detailModal()}
`;
@@ -153,11 +154,13 @@ function renderApp() {
document.querySelector("#logout").addEventListener("click", async () => {
await api("/api/logout", { method: "POST" });
state.user = null;
state.toast = null;
renderLogin();
});
document.querySelector("#refresh-board").addEventListener("click", async () => {
await Promise.all([loadTasks(), loadSettings()]);
renderApp();
showToast("Aktualisiert");
});
if (state.view === "board") bindBoard();
if (state.view === "settings") bindSettings();
@@ -228,7 +231,7 @@ function createModal() {
<div class="field"><label for="task-detail">Beschreibung</label><textarea id="task-detail" name="detail" class="field-input field-textarea" rows="3" placeholder="Details zur Aufgabe..."></textarea></div>
<div class="field-row">
<div class="field"><label for="task-priority">Prioritaet</label><select id="task-priority" name="priority" class="field-input field-select">${priorities.map((p) => `<option ${p === "Medium" ? "selected" : ""}>${p}</option>`).join("")}</select></div>
<div class="field"><label for="task-assignee">Zugewiesen an</label><select id="task-assignee" name="assignee" class="field-input field-select">${assignees.map((a) => `<option value="${a.value}" ${a.value === "bao" ? "selected" : ""}>${a.label}</option>`).join("")}</select></div>
<div class="field"><label for="task-assignee">Zugewiesen an</label><select id="task-assignee" name="assignee" class="field-input field-select">${assignees.map((a) => `<option value="${a.value}" ${a.value === "luna" ? "selected" : ""}>${a.label}</option>`).join("")}</select></div>
</div>
<p class="form-error" id="form-error"></p>
<div class="modal-actions"><button type="button" class="btn-cancel" id="cancel-modal">Abbrechen</button><button type="submit" class="btn-submit">Aufgabe erstellen</button></div>
@@ -264,7 +267,7 @@ function detailModal() {
<div class="sidebar-heading">Eigenschaften</div>
<label class="sidebar-field"><span>Status</span><select name="state" class="field-input field-select slim">${states.map((s) => `<option value="${s}" ${s === task.state ? "selected" : ""}>${stateLabel(s)}</option>`).join("")}</select></label>
<label class="sidebar-field"><span>Prioritaet</span><select name="priority" class="field-input field-select slim">${priorities.map((p) => `<option ${p === task.priority ? "selected" : ""}>${p}</option>`).join("")}</select></label>
<label class="sidebar-field"><span>Zustaendig</span><select name="assignee" class="field-input field-select slim">${assignees.map((a) => `<option value="${a.value}" ${a.value === (task.assignee || "bao") ? "selected" : ""}>${a.label}</option>`).join("")}</select></label>
<label class="sidebar-field"><span>Zustaendig</span><select name="assignee" class="field-input field-select slim">${assignees.map((a) => `<option value="${a.value}" ${a.value === normalizeAssignee(task.assignee) ? "selected" : ""}>${a.label}</option>`).join("")}</select></label>
</form>
<section class="sidebar-card subtle"><div class="sidebar-heading">Snapshot</div><dl class="snapshot-list"><div><dt>Quelle</dt><dd>Maxi</dd></div><div><dt>Prioritaet</dt><dd>${escapeHtml(task.priority)}</dd></div><div><dt>Zustaendig</dt><dd>${assigneeLabel(task.assignee)}</dd></div></dl></section>
<p class="detail-flash error" id="detail-error"></p>
@@ -334,8 +337,10 @@ function bindCreateModal() {
closeModal();
await loadTasks();
renderApp();
showToast("Aufgabe erstellt");
} catch (error) {
document.querySelector("#form-error").textContent = error.message;
showToast(error.message, "error");
}
});
}
@@ -357,8 +362,10 @@ function bindDetailModal() {
closeModal();
await loadTasks();
renderApp();
showToast("Aufgabe gespeichert");
} catch (error) {
document.querySelector("#detail-error").textContent = error.message;
showToast(error.message, "error");
}
});
document.querySelector("#delete-detail").addEventListener("click", async () => {
@@ -366,6 +373,7 @@ function bindDetailModal() {
closeModal();
await loadTasks();
renderApp();
showToast("Aufgabe geloescht");
});
}
@@ -373,16 +381,18 @@ async function moveTask(id, nextState) {
await api(`/api/tasks/${id}`, { method: "PATCH", body: JSON.stringify({ state: nextState }) });
await loadTasks();
renderApp();
showToast(`Status: ${stateLabel(nextState)}`);
}
function settingsView() {
const openClaw = state.openClaw || {};
return `
<div class="page-heading">
<div><span class="eyebrow">MAXI</span><h1>Settings</h1><p>Admin-Konfiguration und Maxi-OpenClaw-Verbindung.</p></div>
<button class="refresh" id="refresh-settings">Aktualisieren</button>
</div>
<section class="settings-grid">
<div class="settings-wrap">
<div class="page-heading">
<div><span class="eyebrow">MAXI</span><h1>Settings</h1><p>Admin-Konfiguration und Maxi-OpenClaw-Verbindung.</p></div>
<button class="refresh" id="refresh-settings">Aktualisieren</button>
</div>
<section class="settings-grid">
<article class="module-card">
<div class="module-card-head"><span class="eyebrow">OPENCLAW</span><span class="badge ${openClaw.ok ? "positive" : "warning"}">${openClaw.ok ? "Live" : "Check"}</span></div>
<h3>Gateway</h3>
@@ -392,12 +402,13 @@ function settingsView() {
<article class="module-card">
<div class="module-card-head"><span class="eyebrow">BOARD</span></div>
<form id="settings-form">
<div class="field"><label>Default-Assignee</label><input class="field-input" name="defaultAssignee" value="${escapeAttr(state.settings?.defaultAssignee || "bao")}"></div>
<div class="field"><label>Default-Assignee</label><select class="field-input field-select" name="defaultAssignee">${assignees.map((a) => `<option value="${a.value}" ${a.value === normalizeAssignee(state.settings?.defaultAssignee) ? "selected" : ""}>${a.label}</option>`).join("")}</select></div>
<div class="field"><label>Zeitzone</label><input class="field-input" name="timezone" value="${escapeAttr(state.settings?.timezone || "Europe/Berlin")}"></div>
<div class="modal-actions"><button class="btn-submit" type="submit">Speichern</button></div>
</form>
</article>
</section>
</section>
</div>
`;
}
@@ -405,6 +416,7 @@ function bindSettings() {
document.querySelector("#refresh-settings").addEventListener("click", async () => {
await loadSettings();
renderApp();
showToast("Settings aktualisiert");
});
document.querySelector("#settings-form").addEventListener("submit", async (event) => {
event.preventDefault();
@@ -412,6 +424,7 @@ function bindSettings() {
await api("/api/settings", { method: "PATCH", body: JSON.stringify(data) });
await loadSettings();
renderApp();
showToast("Settings gespeichert");
});
}
@@ -434,15 +447,35 @@ function priorityColor(priority) {
}
function assigneeLabel(assignee) {
const match = assignees.find((item) => item.value === String(assignee || "").toLowerCase());
return match?.label || escapeHtml(assignee || "Bao");
const match = assignees.find((item) => item.value === normalizeAssignee(assignee));
return match?.label || "Luna";
}
function assigneeClass(assignee) {
return normalizeAssignee(assignee) === "maxi" ? "assignee-maxi" : "assignee-luna";
}
function normalizeAssignee(assignee) {
const lower = String(assignee || "").toLowerCase();
if (lower === "iris") return "assignee-iris";
if (lower === "bao") return "assignee-bao";
return "assignee-agent";
return lower === "maxi" ? "maxi" : "luna";
}
function toastView() {
if (!state.toast) return "";
return `<div class="toast-stack" aria-live="polite"><div class="toast ${state.toast.type === "error" ? "toast-error" : "toast-success"}">${escapeHtml(state.toast.message)}</div></div>`;
}
function showToast(message, type = "success") {
state.toast = { message, type, id: Date.now() };
const toastId = state.toast.id;
renderApp();
window.clearTimeout(showToast.timer);
showToast.timer = window.setTimeout(() => {
if (state.toast?.id === toastId) {
state.toast = null;
renderApp();
}
}, 2600);
}
function stateLabel(value) {