ProjectListView + DashboardView: mostrar ID del proyecto (#305) junto al nombre
- pushDraft: épica usa JSON con client_taker + status=false - ToastNotification: sistema de notificaciones toast global - useToast: composable singleton para mostrar/descartar toasts
This commit is contained in:
@@ -0,0 +1,42 @@
|
||||
<script setup lang="ts">
|
||||
import { useToast } from '@/composables/useToast'
|
||||
import { CheckCircle2, XCircle, Info, X } from 'lucide-vue-next'
|
||||
|
||||
const { toasts, dismiss } = useToast()
|
||||
|
||||
const icons: Record<string, any> = {
|
||||
success: CheckCircle2,
|
||||
error: XCircle,
|
||||
info: Info,
|
||||
}
|
||||
|
||||
const colors: Record<string, string> = {
|
||||
success: 'border-l-green-500',
|
||||
error: 'border-l-red-500',
|
||||
info: 'border-l-blue-500',
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="fixed bottom-4 right-4 z-50 flex flex-col gap-2 max-w-sm">
|
||||
<div
|
||||
v-for="t in toasts"
|
||||
:key="t.id"
|
||||
:class="['flex items-start gap-3 p-3 rounded-lg border bg-card border-l-[3px] text-xs shadow-lg animate-[slideIn_0.3s_ease]', colors[t.type]]"
|
||||
>
|
||||
<component :is="icons[t.type]" class="size-4 shrink-0 mt-0.5" :class="t.type === 'success' ? 'text-green-500' : t.type === 'error' ? 'text-red-500' : 'text-blue-500'" />
|
||||
<div class="flex-1 min-w-0">
|
||||
<p class="font-medium text-foreground">{{ t.title }}</p>
|
||||
<p v-if="t.message" class="text-muted-foreground mt-0.5 break-words">{{ t.message }}</p>
|
||||
</div>
|
||||
<button class="text-muted-foreground hover:text-foreground shrink-0" @click="dismiss(t.id)">×</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
@keyframes slideIn {
|
||||
from { transform: translateX(100%); opacity: 0; }
|
||||
to { transform: translateX(0); opacity: 1; }
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,29 @@
|
||||
import { ref } from 'vue'
|
||||
|
||||
export interface Toast {
|
||||
id: string
|
||||
type: 'success' | 'error' | 'info'
|
||||
title: string
|
||||
message?: string
|
||||
}
|
||||
|
||||
const toasts = ref<Toast[]>([])
|
||||
|
||||
let counter = 0
|
||||
|
||||
export function useToast() {
|
||||
function show(type: Toast['type'], title: string, message?: string, duration = 5000) {
|
||||
const id = `toast-${++counter}`
|
||||
toasts.value.push({ id, type, title, message })
|
||||
if (duration > 0) {
|
||||
setTimeout(() => dismiss(id), duration)
|
||||
}
|
||||
}
|
||||
|
||||
function dismiss(id: string) {
|
||||
const idx = toasts.value.findIndex(t => t.id === id)
|
||||
if (idx !== -1) toasts.value.splice(idx, 1)
|
||||
}
|
||||
|
||||
return { toasts, show, dismiss }
|
||||
}
|
||||
@@ -52,6 +52,7 @@ export interface HuDraftRecord {
|
||||
description: string
|
||||
acceptanceCriteria: string
|
||||
priority: string
|
||||
story_points?: number
|
||||
type: string // 'U' = HU, 'E' = Epic, 'F' = Feature, 'T' = Task, 'B' = Bug
|
||||
metadata: string // JSON opcional: { linkedHuTitles?: string[], estimatedStart?: string, estimatedEnd?: string }
|
||||
sourceSessionId?: number
|
||||
|
||||
+50
-39
@@ -11,6 +11,7 @@ import { Button } from '@/components/ui/button'
|
||||
import HuDrafts from '@/components/HuDrafts.vue'
|
||||
import AiProjectChat from '@/components/AiProjectChat.vue'
|
||||
import { analyzeProject, saveAsDrafts } from '@/services/project-analyzer'
|
||||
import { useToast } from '@/composables/useToast'
|
||||
import { getDrafts, deleteDraft, type HuDraftRecord } from '@/services/hu-drafts-db'
|
||||
import { kappa } from '@/services/kappa-api'
|
||||
import { generateAndSavePlan, getQAPlans, type HUQAPlan } from '@/services/qa-analyzer'
|
||||
@@ -36,7 +37,7 @@ const { t } = useI18n()
|
||||
const projects = useProjectsStore()
|
||||
const workItems = useWorkItemsStore()
|
||||
const usersStore = useUsersStore()
|
||||
|
||||
const { show: showToast } = useToast()
|
||||
const project = computed(() => projects.selected)
|
||||
|
||||
// ─── Team members for this project ────────────────────
|
||||
@@ -267,57 +268,64 @@ async function pushDraft(d: HuDraftRecord) {
|
||||
const token = localStorage.getItem('kappa_token')
|
||||
const headers = { 'Content-Type': 'application/json', ...(token ? { 'Authorization': `Bearer ${token}` } : {}) }
|
||||
|
||||
let endpoint: string, body: string
|
||||
|
||||
if (d.type === 'E') {
|
||||
// Push épica: buscar HUs vinculadas ya enviadas
|
||||
// Push épica
|
||||
const meta = JSON.parse(d.metadata || '{}')
|
||||
const linkedHuIds: number[] = []
|
||||
for (const huTitle of meta.linkedHuTitles || []) {
|
||||
const pushedHu = drafts.value.find(x => x.type !== 'E' && (x.title.toLowerCase().trim() === huTitle.toLowerCase().trim()) && x.kappaId)
|
||||
if (pushedHu?.kappaId) linkedHuIds.push(pushedHu.kappaId)
|
||||
}
|
||||
|
||||
const res = await fetch('/api/epicdevelopment/create/', {
|
||||
method: 'POST', headers,
|
||||
body: JSON.stringify({
|
||||
initiative: String(d.projectId), name: d.title,
|
||||
description: d.description ? `<p>${d.description.replace(/\n/g, '</p><p>')}</p>` : '',
|
||||
client_taker: null, hu: linkedHuIds,
|
||||
stimated_start_date: meta.estimatedStart || null,
|
||||
stimated_end_date: meta.estimatedEnd || null,
|
||||
}),
|
||||
endpoint = '/api/epicdevelopment/create/'
|
||||
body = JSON.stringify({
|
||||
initiative: String(d.projectId),
|
||||
name: d.title,
|
||||
description: d.description ? `<p>${d.description.replace(/\n/g, '</p><p>')}</p>` : '',
|
||||
stimated_start_date: meta.estimatedStart || null,
|
||||
stimated_end_date: meta.estimatedEnd || null,
|
||||
client_taker: Number(meta.clientTaker) || null,
|
||||
hu: linkedHuIds,
|
||||
status: false,
|
||||
})
|
||||
if (res.ok) {
|
||||
d.syncStatus = 'pushed'; await dbSaveDraft(d)
|
||||
} else {
|
||||
d.syncStatus = 'draft'; await dbSaveDraft(d)
|
||||
}
|
||||
} else {
|
||||
// Push HU individual
|
||||
const res = await fetch('/api/userstorys/create/', {
|
||||
method: 'POST', headers,
|
||||
body: JSON.stringify({
|
||||
initiative: String(d.projectId), title: d.title,
|
||||
description: d.description ? `<p>${d.description.replace(/\n/g, '</p><p>')}</p>` : '',
|
||||
criterios_aceptacion: d.acceptanceCriteria ? `<p>${d.acceptanceCriteria.replace(/\n/g, '</p><p>')}</p>` : '',
|
||||
story_points: '',
|
||||
priority: d.priority === 'Alta' ? '1' : d.priority === 'Baja' ? '3' : '2',
|
||||
sprint: '', asignado_a: [], client_taker: null,
|
||||
characterization_hu: '', has_impairment: false,
|
||||
epic_development: null, feature: '',
|
||||
initial_date: null, end_date: null,
|
||||
}),
|
||||
// Push HU
|
||||
endpoint = '/api/userstorys/create/'
|
||||
body = JSON.stringify({
|
||||
initiative: String(d.projectId), title: d.title,
|
||||
description: d.description ? `<p>${d.description.replace(/\n/g, '</p><p>')}</p>` : '',
|
||||
criterios_aceptacion: d.acceptanceCriteria ? `<p>${d.acceptanceCriteria.replace(/\n/g, '</p><p>')}</p>` : '',
|
||||
story_points: String(d.story_points ?? ''),
|
||||
priority: d.priority === 'Alta' ? '1' : d.priority === 'Baja' ? '3' : '2',
|
||||
sprint: '', asignado_a: [], client_taker: null,
|
||||
characterization_hu: '', has_impairment: false,
|
||||
epic_development: null, feature: '',
|
||||
initial_date: null, end_date: null,
|
||||
})
|
||||
if (res.ok) {
|
||||
}
|
||||
|
||||
const res = await fetch(endpoint, { method: 'POST', headers, body })
|
||||
if (res.ok) {
|
||||
if (d.type !== 'E') {
|
||||
const created = await res.json()
|
||||
d.kappaId = created.id || undefined
|
||||
d.syncStatus = 'pushed'
|
||||
await dbSaveDraft(d)
|
||||
} else {
|
||||
d.syncStatus = 'draft'; await dbSaveDraft(d)
|
||||
}
|
||||
d.syncStatus = 'pushed'
|
||||
await dbSaveDraft(d)
|
||||
showToast('success', d.type === 'E' ? 'Épica creada en KAPPA' : 'HU creada en KAPPA', d.title.slice(0, 100))
|
||||
} else {
|
||||
const errorText = await res.text().catch(() => 'Error desconocido')
|
||||
console.error(`[Alpha] Error push a KAPPA (${endpoint}): ${res.status} — ${errorText}`)
|
||||
showToast('error', 'Error al crear en KAPPA', errorText.slice(0, 300))
|
||||
d.syncStatus = 'draft'
|
||||
await dbSaveDraft(d)
|
||||
}
|
||||
} catch {
|
||||
d.syncStatus = 'draft'; await dbSaveDraft(d)
|
||||
} catch (e: any) {
|
||||
console.error('[Alpha] Error en pushDraft:', e)
|
||||
showToast('error', 'Error de red', e.message)
|
||||
d.syncStatus = 'draft'
|
||||
await dbSaveDraft(d)
|
||||
} finally {
|
||||
pushingDraftId.value = null
|
||||
await loadDrafts()
|
||||
@@ -420,7 +428,10 @@ const statusLabel = (status: unknown) => {
|
||||
<template>
|
||||
<div v-if="project" class="@container/main flex flex-1 flex-col gap-4 px-4 lg:px-6">
|
||||
<div class="flex flex-col gap-1">
|
||||
<h1 class="text-2xl font-bold tracking-tight">{{ project.name }}</h1>
|
||||
<h1 class="text-2xl font-bold tracking-tight">
|
||||
<span class="font-mono text-sm text-muted-foreground mr-2">#{{ project.id }}</span>
|
||||
{{ project.name }}
|
||||
</h1>
|
||||
<div class="flex items-center gap-2">
|
||||
<Badge v-if="project.key" variant="outline" class="text-xs">{{ project.key }}</Badge>
|
||||
</div>
|
||||
|
||||
@@ -10,6 +10,7 @@ import ProjectListView from "@/views/ProjectListView.vue"
|
||||
import UsersView from "@/views/UsersView.vue"
|
||||
import TranscriptionsView from "@/views/TranscriptionsView.vue"
|
||||
import SettingsView from "@/views/SettingsView.vue"
|
||||
import ToastNotification from "@/components/ToastNotification.vue"
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
@@ -171,5 +172,6 @@ const tabContent: Record<string, { title: string; description: string; cards: {
|
||||
</div>
|
||||
</div>
|
||||
</SidebarInset>
|
||||
<ToastNotification />
|
||||
</SidebarProvider>
|
||||
</template>
|
||||
|
||||
@@ -231,6 +231,7 @@ onMounted(async () => {
|
||||
<!-- Title + Status -->
|
||||
<div class="flex items-start justify-between gap-2">
|
||||
<CardTitle class="text-base">
|
||||
<span class="font-mono text-xs text-muted-foreground mr-1.5">#{{ p.id }}</span>
|
||||
{{ p.initiative_name || p.name || t('projects.unnamedFallback', { id: p.id }) }}
|
||||
</CardTitle>
|
||||
<Badge id="projects-status-badge" :variant="getStatusVariant(p.status)">
|
||||
|
||||
Reference in New Issue
Block a user