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:
@@ -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,
|
||||
|
||||
@@ -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',
|
||||
})
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user