kin: KIN-138-frontend_dev
This commit is contained in:
parent
e1fe41c428
commit
dadd97a5c5
3 changed files with 80 additions and 15 deletions
|
|
@ -11,6 +11,16 @@ const projects = ref<Project[]>([])
|
||||||
const costs = ref<CostEntry[]>([])
|
const costs = ref<CostEntry[]>([])
|
||||||
const loading = ref(true)
|
const loading = ref(true)
|
||||||
const error = ref('')
|
const error = ref('')
|
||||||
|
const projectSearch = ref('')
|
||||||
|
const showNewMenu = ref(false)
|
||||||
|
|
||||||
|
const filteredProjects = computed(() => {
|
||||||
|
const q = projectSearch.value.trim().toLowerCase()
|
||||||
|
if (!q) return projects.value
|
||||||
|
return projects.value.filter(p =>
|
||||||
|
p.id.toLowerCase().includes(q) || p.name.toLowerCase().includes(q)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
// Add project modal
|
// Add project modal
|
||||||
const showAdd = ref(false)
|
const showAdd = ref(false)
|
||||||
|
|
@ -194,27 +204,39 @@ async function createNewProject() {
|
||||||
<h1 class="text-xl font-bold text-gray-100">{{ t('dashboard.title') }}</h1>
|
<h1 class="text-xl font-bold text-gray-100">{{ t('dashboard.title') }}</h1>
|
||||||
<p class="text-sm text-gray-500" v-if="totalCost > 0">{{ t('dashboard.cost_this_week') }}: ${{ totalCost.toFixed(2) }}</p>
|
<p class="text-sm text-gray-500" v-if="totalCost > 0">{{ t('dashboard.cost_this_week') }}: ${{ totalCost.toFixed(2) }}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex gap-2">
|
<div class="relative">
|
||||||
<button @click="showBootstrap = true"
|
<button @click="showNewMenu = !showNewMenu"
|
||||||
class="px-3 py-1.5 text-xs bg-purple-900/50 text-purple-400 border border-purple-800 rounded hover:bg-purple-900">
|
class="px-3 py-1.5 text-xs bg-gray-800 text-gray-300 border border-gray-700 rounded hover:bg-gray-700 flex items-center gap-1">
|
||||||
{{ t('dashboard.bootstrap') }}
|
+ {{ t('dashboard.new_project') }} ▾
|
||||||
</button>
|
|
||||||
<button @click="showNewProject = true"
|
|
||||||
class="px-3 py-1.5 text-xs bg-green-900/50 text-green-400 border border-green-800 rounded hover:bg-green-900">
|
|
||||||
{{ t('dashboard.new_project') }}
|
|
||||||
</button>
|
|
||||||
<button @click="showAdd = true"
|
|
||||||
class="px-3 py-1.5 text-xs bg-gray-800 text-gray-300 border border-gray-700 rounded hover:bg-gray-700">
|
|
||||||
{{ t('dashboard.blank') }}
|
|
||||||
</button>
|
</button>
|
||||||
|
<div v-if="showNewMenu" class="absolute right-0 top-full mt-1 bg-gray-900 border border-gray-700 rounded shadow-lg z-10 min-w-[170px]">
|
||||||
|
<button @click="showNewProject = true; showNewMenu = false"
|
||||||
|
class="w-full text-left px-3 py-2 text-xs text-green-400 hover:bg-gray-800">
|
||||||
|
🔬 {{ t('dashboard.new_project') }}
|
||||||
|
</button>
|
||||||
|
<button @click="showBootstrap = true; showNewMenu = false"
|
||||||
|
class="w-full text-left px-3 py-2 text-xs text-purple-400 hover:bg-gray-800">
|
||||||
|
🚀 {{ t('dashboard.bootstrap') }}
|
||||||
|
</button>
|
||||||
|
<button @click="showAdd = true; showNewMenu = false"
|
||||||
|
class="w-full text-left px-3 py-2 text-xs text-gray-400 hover:bg-gray-800">
|
||||||
|
📋 {{ t('dashboard.blank') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div v-if="!loading && !error" class="mb-3">
|
||||||
|
<input v-model="projectSearch"
|
||||||
|
:placeholder="t('dashboard.search_placeholder', 'Search projects...')"
|
||||||
|
class="w-full bg-gray-800 border border-gray-700 rounded px-3 py-1.5 text-sm text-gray-300 placeholder-gray-600 focus:border-gray-500 outline-none" />
|
||||||
|
</div>
|
||||||
|
|
||||||
<p v-if="loading" class="text-gray-500 text-sm">{{ t('dashboard.loading') }}</p>
|
<p v-if="loading" class="text-gray-500 text-sm">{{ t('dashboard.loading') }}</p>
|
||||||
<p v-else-if="error" class="text-red-400 text-sm">{{ error }}</p>
|
<p v-else-if="error" class="text-red-400 text-sm">{{ error }}</p>
|
||||||
|
|
||||||
<div v-else class="grid gap-3">
|
<div v-else class="grid gap-3">
|
||||||
<div v-for="p in projects" :key="p.id">
|
<div v-for="p in filteredProjects" :key="p.id">
|
||||||
<!-- Inline delete confirmation -->
|
<!-- Inline delete confirmation -->
|
||||||
<div v-if="confirmDeleteId === p.id"
|
<div v-if="confirmDeleteId === p.id"
|
||||||
class="border border-red-800 rounded-lg p-4 bg-red-950/20">
|
class="border border-red-800 rounded-lg p-4 bg-red-950/20">
|
||||||
|
|
@ -272,6 +294,13 @@ async function createNewProject() {
|
||||||
{{ p.total_tasks - p.done_tasks - p.active_tasks - p.blocked_tasks - (p.review_tasks || 0) }} pending
|
{{ p.total_tasks - p.done_tasks - p.active_tasks - p.blocked_tasks - (p.review_tasks || 0) }} pending
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-if="p.total_tasks > 0" class="mt-2 flex items-center gap-2">
|
||||||
|
<div class="flex-1 h-1 bg-gray-800 rounded-full overflow-hidden">
|
||||||
|
<div class="h-full bg-green-700 rounded-full transition-all"
|
||||||
|
:style="{ width: `${Math.round((p.done_tasks || 0) / p.total_tasks * 100)}%` }"></div>
|
||||||
|
</div>
|
||||||
|
<span class="text-[10px] text-gray-600 shrink-0">{{ Math.round((p.done_tasks || 0) / p.total_tasks * 100) }}%</span>
|
||||||
|
</div>
|
||||||
</router-link>
|
</router-link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -646,6 +646,18 @@ const taskCategories = computed(() => {
|
||||||
return Array.from(cats).sort()
|
return Array.from(cats).sort()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const taskStats = computed(() => {
|
||||||
|
const tasks = project.value?.tasks || []
|
||||||
|
const total = tasks.length
|
||||||
|
if (!total) return null
|
||||||
|
const done = tasks.filter(t => t.status === 'done').length
|
||||||
|
const running = tasks.filter(t => t.status === 'in_progress').length
|
||||||
|
const review = tasks.filter(t => t.status === 'review').length
|
||||||
|
const blocked = tasks.filter(t => t.status === 'blocked').length
|
||||||
|
const pending = tasks.filter(t => t.status === 'pending').length
|
||||||
|
return { total, done, running, review, blocked, pending, pct: Math.round(done / total * 100) }
|
||||||
|
})
|
||||||
|
|
||||||
const searchFilteredTasks = computed(() => {
|
const searchFilteredTasks = computed(() => {
|
||||||
if (!project.value) return []
|
if (!project.value) return []
|
||||||
const q = taskSearch.value.trim().toLowerCase()
|
const q = taskSearch.value.trim().toLowerCase()
|
||||||
|
|
@ -1079,6 +1091,24 @@ async function addDecision() {
|
||||||
|
|
||||||
<!-- Tasks Tab -->
|
<!-- Tasks Tab -->
|
||||||
<div v-if="activeTab === 'tasks'">
|
<div v-if="activeTab === 'tasks'">
|
||||||
|
<!-- Progress stats bar -->
|
||||||
|
<div v-if="taskStats && taskStats.total > 0" class="mb-3 px-3 py-2 border border-gray-800 rounded-lg bg-gray-900/30">
|
||||||
|
<div class="flex items-center gap-2 mb-1.5">
|
||||||
|
<div class="flex-1 h-1.5 bg-gray-800 rounded-full overflow-hidden flex">
|
||||||
|
<div class="h-full bg-green-700 transition-all rounded-l-full"
|
||||||
|
:style="{ width: `${taskStats.pct}%` }"></div>
|
||||||
|
<div v-if="taskStats.running" class="h-full bg-blue-500 animate-pulse transition-all"
|
||||||
|
:style="{ width: `${Math.round(taskStats.running / taskStats.total * 100)}%` }"></div>
|
||||||
|
</div>
|
||||||
|
<span class="text-xs text-gray-500 shrink-0">{{ taskStats.done }}/{{ taskStats.total }} ({{ taskStats.pct }}%)</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-3 text-[11px]">
|
||||||
|
<span v-if="taskStats.running" class="text-blue-400">● {{ taskStats.running }} running</span>
|
||||||
|
<span v-if="taskStats.review" class="text-yellow-400">⚠ {{ taskStats.review }} review</span>
|
||||||
|
<span v-if="taskStats.blocked" class="text-red-400">✕ {{ taskStats.blocked }} blocked</span>
|
||||||
|
<span v-if="taskStats.pending" class="text-gray-500">○ {{ taskStats.pending }} pending</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="flex flex-col gap-2 mb-3">
|
<div class="flex flex-col gap-2 mb-3">
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<div class="flex gap-1 flex-wrap items-center">
|
<div class="flex gap-1 flex-wrap items-center">
|
||||||
|
|
|
||||||
|
|
@ -461,8 +461,14 @@ async function saveEdit() {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="task.brief && !isManualEscalation" class="text-xs text-gray-500 mb-1">
|
<div v-if="task.brief && !isManualEscalation" class="text-xs mb-1">
|
||||||
Brief: {{ JSON.stringify(task.brief) }}
|
<span class="text-gray-600">Brief: </span>
|
||||||
|
<span v-if="(task.brief as Record<string, unknown>).text" class="text-gray-400 whitespace-pre-wrap">{{ (task.brief as Record<string, unknown>).text }}</span>
|
||||||
|
<span v-else-if="(task.brief as Record<string, unknown>).description" class="text-gray-400 whitespace-pre-wrap">{{ (task.brief as Record<string, unknown>).description }}</span>
|
||||||
|
<details v-else class="inline">
|
||||||
|
<summary class="text-gray-600 cursor-pointer hover:text-gray-400">raw</summary>
|
||||||
|
<pre class="text-gray-600 font-mono text-[10px] mt-1 whitespace-pre-wrap">{{ JSON.stringify(task.brief, null, 2) }}</pre>
|
||||||
|
</details>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="task.acceptance_criteria" class="mb-2 px-3 py-2 border border-gray-700 bg-gray-900/40 rounded">
|
<div v-if="task.acceptance_criteria" class="mb-2 px-3 py-2 border border-gray-700 bg-gray-900/40 rounded">
|
||||||
<div class="text-xs font-semibold text-gray-400 mb-1">{{ t('taskDetail.acceptance_criteria') }}</div>
|
<div class="text-xs font-semibold text-gray-400 mb-1">{{ t('taskDetail.acceptance_criteria') }}</div>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue