Refine Maxi task board UX
This commit is contained in:
+50
-17
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user