feat: ship agent progress visibility
This commit is contained in:
@@ -12,9 +12,10 @@ import { computed, onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import {
|
||||
ArrowLeft, Clock3, CalendarDays, ListChecks, Save, Plus,
|
||||
X, Link2, MessageSquare, Trash2, CheckCircle, AlertCircle,
|
||||
X, Link2, MessageSquare, Trash2, CheckCircle, AlertCircle, ShieldBan,
|
||||
} from '@lucide/vue'
|
||||
import { apiFetch } from '../services/api'
|
||||
import { useAuthStore } from '../stores/auth'
|
||||
|
||||
/* ── Types ──────────────────────────────────── */
|
||||
interface TaskDto {
|
||||
@@ -29,6 +30,10 @@ interface TaskDto {
|
||||
dueDate: string | null
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
isAgentTask?: boolean
|
||||
expectedFrom?: string | null
|
||||
lastActivityMessage?: string | null
|
||||
lastActivityAt?: string | null
|
||||
}
|
||||
|
||||
interface ActivityEntry {
|
||||
@@ -42,6 +47,7 @@ interface ActivityEntry {
|
||||
/* ── State ───────────────────────────────────── */
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const authStore = useAuthStore()
|
||||
const taskId = computed(() => route.params.id as string)
|
||||
|
||||
const task = ref<TaskDto | null>(null)
|
||||
@@ -79,6 +85,7 @@ const postingComment = ref(false)
|
||||
|
||||
/* ── Delete state ───────────────────────────── */
|
||||
const deletingTask = ref('') // id of subtask being deleted
|
||||
const canChangeState = computed(() => authStore.isIris || authStore.isBao)
|
||||
|
||||
/* ── Helpers ────────────────────────────────── */
|
||||
function statusLabel(state: string): string {
|
||||
@@ -129,6 +136,22 @@ function toDateInput(date?: string | null): string {
|
||||
return new Date(date).toISOString().slice(0, 10)
|
||||
}
|
||||
|
||||
function relativeTime(date?: string | null): string {
|
||||
if (!date) return 'keine Updates'
|
||||
const diffMs = Date.now() - new Date(date).getTime()
|
||||
const mins = Math.max(0, Math.round(diffMs / 60000))
|
||||
if (mins < 1) return 'gerade eben'
|
||||
if (mins < 60) return `vor ${mins} min`
|
||||
const hours = Math.round(mins / 60)
|
||||
if (hours < 24) return `vor ${hours} h`
|
||||
const days = Math.round(hours / 24)
|
||||
return `vor ${days} d`
|
||||
}
|
||||
|
||||
function progressHint(taskLike: Pick<TaskDto, 'lastActivityMessage' | 'expectedFrom'>): string {
|
||||
return taskLike.lastActivityMessage?.trim() || (taskLike.expectedFrom ? `Wartet auf ${taskLike.expectedFrom}` : 'Noch kein relevanter Progress-Status')
|
||||
}
|
||||
|
||||
/* ── API calls ───────────────────────────────── */
|
||||
async function loadTask() {
|
||||
loading.value = true
|
||||
@@ -188,12 +211,16 @@ async function saveTask() {
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
||||
// Also update state if changed
|
||||
if (form.state !== task.value.state) {
|
||||
const stateRes = await apiFetch(`/api/dashboard/tasks/${taskId.value}/status`, {
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ status: form.state }),
|
||||
})
|
||||
if (!stateRes.ok) throw new Error('Status-Update fehlgeschlagen')
|
||||
if (!canChangeState.value) {
|
||||
form.state = task.value.state
|
||||
} else {
|
||||
const stateRes = await apiFetch(`/api/dashboard/tasks/${taskId.value}/status`, {
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ status: form.state }),
|
||||
})
|
||||
if (!stateRes.ok) throw new Error('Status-Update fehlgeschlagen')
|
||||
}
|
||||
}
|
||||
saveSuccess.value = true
|
||||
await loadTask()
|
||||
@@ -236,6 +263,7 @@ async function createSubtask() {
|
||||
}
|
||||
|
||||
async function updateSubtaskState(subtaskId: string, newState: string) {
|
||||
if (!canChangeState.value) return
|
||||
try {
|
||||
await apiFetch(`/api/dashboard/tasks/${subtaskId}/status`, {
|
||||
method: 'PATCH',
|
||||
@@ -332,9 +360,16 @@ function handleKeydown(e: KeyboardEvent) {
|
||||
<span class="state-badge" :class="statusClass(task.state)">{{ statusLabel(task.state) }}</span>
|
||||
<span class="meta-chip" v-if="task.source">Quelle: {{ task.source }}</span>
|
||||
<span class="meta-chip">{{ task.priority }} Priorität</span>
|
||||
<span v-if="task.isAgentTask" class="meta-chip">🤖 Agent-Task</span>
|
||||
<span v-if="task.expectedFrom" class="meta-chip">⏳ Erwartet: {{ task.expectedFrom }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="!canChangeState" class="readonly-banner">
|
||||
<ShieldBan :size="14" />
|
||||
Inhalte sind editierbar, Statuswechsel bleiben Bao/Iris vorbehalten.
|
||||
</div>
|
||||
|
||||
<div class="detail-body">
|
||||
<!-- Main Column -->
|
||||
<div class="detail-main">
|
||||
@@ -349,6 +384,14 @@ function handleKeydown(e: KeyboardEvent) {
|
||||
<CalendarDays :size="13" />
|
||||
Aktualisiert {{ formatDate(task.updatedAt, true) }}
|
||||
</span>
|
||||
<span v-if="task.isAgentTask">
|
||||
<MessageSquare :size="13" />
|
||||
Letzter Status {{ relativeTime(task.lastActivityAt ?? task.updatedAt) }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div v-if="task.isAgentTask" class="progress-banner">
|
||||
<strong>Letzter Fortschritt:</strong> {{ progressHint(task) }}
|
||||
</div>
|
||||
|
||||
<!-- Description -->
|
||||
@@ -407,6 +450,7 @@ function handleKeydown(e: KeyboardEvent) {
|
||||
<select
|
||||
:value="child.state"
|
||||
class="state-select-mini"
|
||||
:disabled="!canChangeState"
|
||||
@change="updateSubtaskState(child.id, ($event.target as HTMLSelectElement).value)"
|
||||
>
|
||||
<option value="Backlog">Offen</option>
|
||||
@@ -473,7 +517,7 @@ function handleKeydown(e: KeyboardEvent) {
|
||||
<div class="sidebar-head">Eigenschaften</div>
|
||||
<label class="sidebar-field">
|
||||
<span>Status</span>
|
||||
<select v-model="form.state" class="galaxy-input galaxy-select">
|
||||
<select v-model="form.state" class="galaxy-input galaxy-select" :disabled="!canChangeState">
|
||||
<option value="Backlog">Offen</option>
|
||||
<option value="In progress">In Bearbeitung</option>
|
||||
<option value="Delegated">Delegiert</option>
|
||||
@@ -514,6 +558,7 @@ function handleKeydown(e: KeyboardEvent) {
|
||||
<div><dt>Quelle</dt><dd>{{ task.source || '—' }}</dd></div>
|
||||
<div><dt>Erstellt</dt><dd>{{ formatDate(task.createdAt) }}</dd></div>
|
||||
<div><dt>Geändert</dt><dd>{{ formatDate(task.updatedAt, true) }}</dd></div>
|
||||
<div v-if="task.isAgentTask"><dt>Letzter Status</dt><dd>{{ relativeTime(task.lastActivityAt ?? task.updatedAt) }}</dd></div>
|
||||
</dl>
|
||||
</section>
|
||||
|
||||
@@ -647,6 +692,19 @@ function handleKeydown(e: KeyboardEvent) {
|
||||
border: 1px solid rgba(150, 140, 255, 0.08);
|
||||
}
|
||||
|
||||
.readonly-banner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 16px;
|
||||
padding: 10px 12px;
|
||||
border-radius: 10px;
|
||||
background: rgba(147,51,234,.08);
|
||||
border: 1px solid rgba(147,51,234,.18);
|
||||
color: #c084fc;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* ── Detail Body Grid ────────────────────────── */
|
||||
.detail-body {
|
||||
display: grid;
|
||||
@@ -694,6 +752,15 @@ function handleKeydown(e: KeyboardEvent) {
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.progress-banner {
|
||||
padding: 12px 14px;
|
||||
border-radius: 12px;
|
||||
background: rgba(124, 108, 255, 0.08);
|
||||
border: 1px solid rgba(124, 108, 255, 0.14);
|
||||
color: #a8a3d6;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* ── Sections ──────────────────────────── */
|
||||
.section-card {
|
||||
border: 1px solid rgba(150, 140, 255, 0.10);
|
||||
@@ -830,6 +897,11 @@ function handleKeydown(e: KeyboardEvent) {
|
||||
color: #ece9ff;
|
||||
}
|
||||
|
||||
.state-select-mini:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.loading-mini {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -1025,6 +1097,11 @@ function handleKeydown(e: KeyboardEvent) {
|
||||
color: #ece9ff;
|
||||
}
|
||||
|
||||
.galaxy-select:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
|
||||
Reference in New Issue
Block a user