feat: Add 'atas' (minutes/records) management feature, and implement various improvements across UI, backend logic, and authentication.

This commit is contained in:
2025-12-02 16:37:48 -03:00
parent 05e7f1181d
commit 4bd9e21748
265 changed files with 29156 additions and 26460 deletions

View File

@@ -1,103 +1,108 @@
"use node";
'use node';
import { action } from "../_generated/server";
import { v } from "convex/values";
import { internal } from "../_generated/api";
import { v } from 'convex/values';
import { internal } from '../_generated/api';
import { action } from '../_generated/server';
/**
* Enviar push notification usando Web Push API
*/
export const enviarPush = action({
args: {
subscriptionId: v.id("pushSubscriptions"),
titulo: v.string(),
corpo: v.string(),
data: v.optional(
v.object({
conversaId: v.optional(v.string()),
mensagemId: v.optional(v.string()),
tipo: v.optional(v.string()),
})
),
},
returns: v.object({ sucesso: v.boolean(), erro: v.optional(v.string()) }),
handler: async (ctx, args) => {
try {
// Buscar subscription
const subscription = await ctx.runQuery(internal.pushNotifications.getSubscriptionById, {
subscriptionId: args.subscriptionId,
});
args: {
subscriptionId: v.id('pushSubscriptions'),
titulo: v.string(),
corpo: v.string(),
data: v.optional(
v.object({
conversaId: v.optional(v.string()),
mensagemId: v.optional(v.string()),
tipo: v.optional(v.string())
})
)
},
returns: v.object({ sucesso: v.boolean(), erro: v.optional(v.string()) }),
handler: async (ctx, args) => {
try {
// Buscar subscription
const subscription = await ctx.runQuery(internal.pushNotifications.getSubscriptionById, {
subscriptionId: args.subscriptionId
});
if (!subscription || !subscription.ativo) {
return { sucesso: false, erro: "Subscription não encontrada ou inativa" };
}
if (!subscription || !subscription.ativo) {
return {
sucesso: false,
erro: 'Subscription não encontrada ou inativa'
};
}
// Web Push requer VAPID keys (deve estar em variáveis de ambiente)
// Por enquanto, vamos usar uma implementação básica
// Em produção, você precisará configurar VAPID keys
// Web Push requer VAPID keys (deve estar em variáveis de ambiente)
// Por enquanto, vamos usar uma implementação básica
// Em produção, você precisará configurar VAPID keys
const webpushModule = await import("web-push");
// web-push pode exportar como default ou named exports
// Usar a declaração de tipo do módulo web-push
interface WebPushType {
setVapidDetails: (subject: string, publicKey: string, privateKey: string) => void;
sendNotification: (
subscription: { endpoint: string; keys: { p256dh: string; auth: string } },
payload: string | Buffer
) => Promise<void>;
}
const webpush: WebPushType = (webpushModule.default || webpushModule) as WebPushType;
const webpushModule = await import('web-push');
// web-push pode exportar como default ou named exports
// Usar a declaração de tipo do módulo web-push
interface WebPushType {
setVapidDetails: (subject: string, publicKey: string, privateKey: string) => void;
sendNotification: (
subscription: {
endpoint: string;
keys: { p256dh: string; auth: string };
},
payload: string | Buffer
) => Promise<void>;
}
const webpush: WebPushType = (webpushModule.default || webpushModule) as WebPushType;
// VAPID keys devem vir de variáveis de ambiente
const publicKey: string | undefined = process.env.VAPID_PUBLIC_KEY;
const privateKey: string | undefined = process.env.VAPID_PRIVATE_KEY;
// VAPID keys devem vir de variáveis de ambiente
const publicKey: string | undefined = process.env.VAPID_PUBLIC_KEY;
const privateKey: string | undefined = process.env.VAPID_PRIVATE_KEY;
if (!publicKey || !privateKey) {
console.warn("⚠️ VAPID keys não configuradas. Push notifications não funcionarão.");
// Em desenvolvimento, podemos retornar sucesso sem enviar
return { sucesso: true };
}
if (!publicKey || !privateKey) {
console.warn('⚠️ VAPID keys não configuradas. Push notifications não funcionarão.');
// Em desenvolvimento, podemos retornar sucesso sem enviar
return { sucesso: true };
}
webpush.setVapidDetails("mailto:suporte@sgse.app", publicKey, privateKey);
webpush.setVapidDetails('mailto:suporte@sgse.app', publicKey, privateKey);
// Preparar payload da notificação
const payload = JSON.stringify({
title: args.titulo,
body: args.corpo,
icon: "/favicon.png",
badge: "/favicon.png",
data: args.data || {},
tag: args.data?.conversaId || "default",
requireInteraction: args.data?.tipo === "mencao", // Menções requerem interação
});
// Preparar payload da notificação
const payload = JSON.stringify({
title: args.titulo,
body: args.corpo,
icon: '/favicon.png',
badge: '/favicon.png',
data: args.data || {},
tag: args.data?.conversaId || 'default',
requireInteraction: args.data?.tipo === 'mencao' // Menções requerem interação
});
// Enviar push notification
await webpush.sendNotification(
{
endpoint: subscription.endpoint,
keys: {
p256dh: subscription.keys.p256dh,
auth: subscription.keys.auth,
},
},
payload
);
// Enviar push notification
await webpush.sendNotification(
{
endpoint: subscription.endpoint,
keys: {
p256dh: subscription.keys.p256dh,
auth: subscription.keys.auth
}
},
payload
);
console.log(`✅ Push notification enviada para ${subscription.endpoint}`);
return { sucesso: true };
} catch (error: unknown) {
const errorMessage = error instanceof Error ? error.message : String(error);
console.error("❌ Erro ao enviar push notification:", errorMessage);
console.log(`✅ Push notification enviada para ${subscription.endpoint}`);
return { sucesso: true };
} catch (error: unknown) {
const errorMessage = error instanceof Error ? error.message : String(error);
console.error('❌ Erro ao enviar push notification:', errorMessage);
// Se subscription inválida, marcar como inativa
if (errorMessage.includes("410") || errorMessage.includes("expired")) {
await ctx.runMutation(internal.pushNotifications.marcarSubscriptionInativa, {
subscriptionId: args.subscriptionId,
});
}
// Se subscription inválida, marcar como inativa
if (errorMessage.includes('410') || errorMessage.includes('expired')) {
await ctx.runMutation(internal.pushNotifications.marcarSubscriptionInativa, {
subscriptionId: args.subscriptionId
});
}
return { sucesso: false, erro: errorMessage };
}
},
return { sucesso: false, erro: errorMessage };
}
}
});