Teams integration: webhook messaging + configuración en Settings + plugin-http

- @tauri-apps/plugin-http instalado (npm + Cargo.toml + capabilities)
- src-tauri/src/main.rs: registrado tauri_plugin_http::init()
- services/teams.ts: servicio para enviar mensajes Teams via webhook
  soporta Tauri plugin y browser fetch con fallback automático
  incluye notifyHUCreated, notifyBlockerLogged, sendTestMessage
- SettingsView: nueva sección Teams con input de webhook URL + botón test
- ChartAreaInteractive.vue: removido import no usado (@unovis/vue)
This commit is contained in:
2026-05-29 23:55:37 -05:00
parent bf81b8e04b
commit b21214d1f1
13 changed files with 6919 additions and 15 deletions
@@ -3,7 +3,6 @@ import { ref, computed } from "vue"
import type { ChartConfig } from "@/components/ui/chart"
// import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from "recharts"
import { VisArea, VisAxis, VisLine, VisXYContainer } from "@unovis/vue"
import {
Card,
CardContent,
+138
View File
@@ -0,0 +1,138 @@
import { storage } from '@/services/storage'
const STORAGE_KEY = 'teams_webhook_url'
export function getWebhookUrl(): string {
return storage.get(STORAGE_KEY) || ''
}
export function setWebhookUrl(url: string): void {
storage.set(STORAGE_KEY, url)
}
export function hasWebhook(): boolean {
return !!getWebhookUrl()
}
interface TeamsMessage {
title?: string
text: string
themeColor?: string
sections?: {
activityTitle?: string
activitySubtitle?: string
facts?: { name: string; value: string }[]
text?: string
}[]
}
/**
* Envía un mensaje a Teams via webhook.
* Formato: https://learn.microsoft.com/en-us/microsoftteams/platform/webhooks-and-connectors/how-to/add-incoming-webhook
* Soporta tanto Tauri (plugin-http) como browser (fetch).
*/
export async function sendToTeams(message: TeamsMessage): Promise<boolean> {
const webhookUrl = getWebhookUrl()
if (!webhookUrl) {
console.warn('[Teams] No hay webhook configurado')
return false
}
const payload = {
'@type': 'MessageCard',
'@context': 'http://schema.org/extensions',
summary: message.title || message.text.slice(0, 50),
title: message.title,
text: message.text,
themeColor: message.themeColor || '0078D4',
sections: message.sections || [],
}
try {
// Intentar usar Tauri plugin HTTP primero
if (typeof window !== 'undefined' && (window as any).__TAURI_INTERNALS__) {
const { fetch } = await import('@tauri-apps/plugin-http')
const res = await fetch(webhookUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
})
if (!res.ok) {
console.error(`[Teams] Error HTTP ${res.status}`)
return false
}
console.log('[Teams] Mensaje enviado via Tauri plugin')
return true
}
// Fallback browser fetch
const res = await fetch(webhookUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
})
if (!res.ok) {
console.error(`[Teams] Error HTTP ${res.status}`)
return false
}
console.log('[Teams] Mensaje enviado via browser fetch')
return true
} catch (e: any) {
console.error('[Teams] Error al enviar mensaje:', e)
return false
}
}
/**
* Envía una notificación de HU creada.
*/
export async function notifyHUCreated(projectName: string, huTitle: string, huUrl?: string): Promise<boolean> {
return sendToTeams({
title: '🆕 Nueva HU creada',
text: `**Proyecto:** ${projectName}`,
themeColor: '0078D4',
sections: [{
facts: [
{ name: 'HU', value: huTitle },
{ name: 'Proyecto', value: projectName },
{ name: 'URL', value: huUrl || window.location.href },
],
}],
})
}
/**
* Envía una notificación de bloqueo/impedimento.
*/
export async function notifyBlockerLogged(
projectName: string,
huTitle: string,
category: string,
description: string,
hoursLost: number,
): Promise<boolean> {
return sendToTeams({
title: '⛔ Bloqueo registrado',
text: `**Proyecto:** ${projectName}`,
themeColor: 'FF4444',
sections: [{
facts: [
{ name: 'HU', value: huTitle },
{ name: 'Categoría', value: category },
{ name: 'Descripción', value: description },
{ name: 'Horas perdidas', value: `${hoursLost}h` },
],
}],
})
}
/**
* Envía un mensaje de prueba.
*/
export async function sendTestMessage(): Promise<boolean> {
return sendToTeams({
title: '🔔 Prueba de integración',
text: 'Este es un mensaje de prueba desde **Alpha**.\n\nSi ves esto, la integración con Teams funciona correctamente.',
themeColor: '00FF00',
})
}
+67
View File
@@ -16,6 +16,7 @@ import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Badge } from '@/components/ui/badge'
import { Separator } from '@/components/ui/separator'
import { getWebhookUrl, setWebhookUrl, sendTestMessage } from '@/services/teams'
import {
Brain,
Key,
@@ -26,6 +27,7 @@ import {
LogOut,
Sparkles,
ChevronRight,
MessageSquare,
} from 'lucide-vue-next'
const { t } = useI18n()
@@ -68,6 +70,33 @@ function markDirty(key: string) {
onMounted(loadPrompts)
// ─── Teams ───────────────────────────────────────────
const teamsUrl = ref(getWebhookUrl())
const teamsSending = ref(false)
const teamsStatus = ref<'idle' | 'ok' | 'error'>('idle')
const teamsStatusMsg = ref('')
function teamsUrlChanged() {
setWebhookUrl(teamsUrl.value)
teamsStatus.value = 'idle'
}
async function testTeams() {
teamsSending.value = true
teamsStatus.value = 'idle'
teamsStatusMsg.value = ''
try {
const ok = await sendTestMessage()
teamsStatus.value = ok ? 'ok' : 'error'
teamsStatusMsg.value = ok ? 'Mensaje enviado a Teams' : 'Error al enviar. Verifica la URL del webhook.'
} catch (e: any) {
teamsStatus.value = 'error'
teamsStatusMsg.value = e.message
} finally {
teamsSending.value = false
}
}
const newKey = ref('')
const keySaved = ref(false)
@@ -283,6 +312,44 @@ const tierColors: Record<string, string> = {
</CardContent>
</Card>
<!-- Teams -->
<Card id="settings-teams">
<CardHeader>
<CardTitle class="text-sm font-medium flex items-center gap-2">
<MessageSquare class="size-4" />
Microsoft Teams
</CardTitle>
<CardDescription class="text-xs">
Configurá un webhook para recibir notificaciones de Alpha en Teams.
</CardDescription>
</CardHeader>
<CardContent class="space-y-3">
<div class="space-y-1.5">
<Label class="text-xs font-medium">URL del Webhook</Label>
<Input
v-model="teamsUrl"
placeholder="https://...office.com/webhookb2/..."
class="text-xs font-mono"
@input="teamsUrlChanged"
/>
<p class="text-[10px] text-muted-foreground">
Creá un webhook en Teams: Canal ... Conectores Webhook entrante
</p>
</div>
<div v-if="teamsUrl" class="flex items-center gap-2">
<Button size="sm" variant="outline" class="text-xs" :disabled="teamsSending" @click="testTeams">
{{ teamsSending ? 'Enviando...' : 'Enviar prueba' }}
</Button>
<span v-if="teamsStatus === 'ok'" class="text-xs text-green-600 flex items-center gap-1">
<CheckCircle2 class="size-3" /> {{ teamsStatusMsg }}
</span>
<span v-if="teamsStatus === 'error'" class="text-xs text-red-600 flex items-center gap-1">
<XCircle class="size-3" /> {{ teamsStatusMsg }}
</span>
</div>
</CardContent>
</Card>
<!-- Prompts -->
<Card id="settings-prompts" class="border-dashed">
<CardHeader>