Dashboard-Automatizase/app/dashboard/page.tsx
Luis Erlacher 0152a2fda0 feat: add n8n API testing script for Google OAuth2 schema and existing credentials
- Implemented a bash script to test n8n API and retrieve credential schemas.
- Added types for API responses, Google Calendar, and WhatsApp instances.
- Configured Vitest for testing with React and added setup for testing-library.
2025-10-10 14:29:02 -03:00

330 lines
10 KiB
TypeScript

"use client";
import { useCallback, useEffect, useState } from "react";
import ConfirmModal from "@/components/ConfirmModal";
import GoogleCalendarCard from "@/components/GoogleCalendarCard";
import QRCodeModal from "@/components/QRCodeModal";
import Toast from "@/components/Toast";
import WhatsAppInstanceCard from "@/components/WhatsAppInstanceCard";
import type { InstanceStatus } from "@/lib/evolutionapi";
import { supabase } from "@/lib/supabase";
export default function DashboardPage() {
const [userName, setUserName] = useState<string>("Usuário");
const [instances, setInstances] = useState<InstanceStatus[]>([]);
const [loading, setLoading] = useState(true);
// QR Code Modal State
const [qrModalOpen, setQrModalOpen] = useState(false);
const [qrCode, setQrCode] = useState<string | null>(null);
const [qrInstanceName, setQrInstanceName] = useState<string>("");
const [qrLoading, setQrLoading] = useState(false);
const [qrError, setQrError] = useState<string | null>(null);
// Disconnect Confirmation State
const [confirmModalOpen, setConfirmModalOpen] = useState(false);
const [disconnectInstanceName, setDisconnectInstanceName] =
useState<string>("");
const [disconnectLoading, setDisconnectLoading] = useState(false);
// Toast State
const [toastVisible, setToastVisible] = useState(false);
const [toastMessage, setToastMessage] = useState("");
const [toastType, setToastType] = useState<"success" | "error">("success");
// Google Calendar State
const [calendarConnected, setCalendarConnected] = useState(false);
const [calendarEmail, setCalendarEmail] = useState<string | null>(null);
const [calendarLoading, setCalendarLoading] = useState(true);
const [calendarConnecting, _setCalendarConnecting] = useState(false);
const loadInstances = useCallback(async () => {
setLoading(true);
try {
const response = await fetch("/api/instances");
if (!response.ok) {
throw new Error("Failed to fetch instances");
}
const data: InstanceStatus[] = await response.json();
setInstances(data);
} catch (error) {
console.error("Error loading instances:", error);
} finally {
setLoading(false);
}
}, []);
const loadGoogleCalendarStatus = useCallback(async () => {
setCalendarLoading(true);
try {
const {
data: { user },
} = await supabase.auth.getUser();
if (user) {
const { data, error } = await supabase
.schema("portal")
.from("integrations")
.select("status, connected_at")
.eq("user_id", user.id)
.eq("provider", "google_calendar")
.maybeSingle();
if (error && error.code !== "PGRST116") {
// PGRST116 = no rows returned
throw error;
}
if (data && data.status === "connected") {
setCalendarConnected(true);
// Optionally fetch email from Google API or store it in Supabase
// For now, we can leave it null or fetch from user metadata
setCalendarEmail(null); // TODO: implement email fetching if needed
} else {
setCalendarConnected(false);
setCalendarEmail(null);
}
}
} catch (error) {
console.error("Error loading Google Calendar status:", error);
setCalendarConnected(false);
} finally {
setCalendarLoading(false);
}
}, []);
useEffect(() => {
const getUser = async () => {
const {
data: { user },
} = await supabase.auth.getUser();
if (user?.email) {
const name = user.email.split("@")[0];
setUserName(name);
}
};
const init = async () => {
await getUser();
await Promise.all([loadInstances(), loadGoogleCalendarStatus()]);
};
init();
// Check for OAuth callback
const urlParams = new URLSearchParams(window.location.search);
const oauthSuccess = urlParams.get("oauth_success");
const oauthError = urlParams.get("oauth_error");
if (oauthSuccess === "true") {
setToastMessage("Google Calendar conectado com sucesso!");
setToastType("success");
setToastVisible(true);
window.history.replaceState({}, "", "/dashboard");
loadGoogleCalendarStatus(); // Reload status
} else if (oauthError) {
setToastMessage("Erro ao conectar Google Calendar. Tente novamente.");
setToastType("error");
setToastVisible(true);
window.history.replaceState({}, "", "/dashboard");
}
}, [loadInstances, loadGoogleCalendarStatus]);
const handleGenerateQR = async (instanceName: string) => {
setQrInstanceName(instanceName);
setQrModalOpen(true);
setQrCode(null);
setQrError(null);
setQrLoading(true);
try {
const response = await fetch(`/api/instances/${instanceName}/qr`);
if (!response.ok) {
throw new Error("Failed to generate QR code");
}
const data = await response.json();
setQrCode(data.qrcode || data.base64 || data.code);
} catch (error: unknown) {
setQrError("Falha ao gerar QR code. Tente novamente.");
console.error("QR Code generation error:", error);
} finally {
setQrLoading(false);
}
};
const handleCloseQrModal = () => {
setQrModalOpen(false);
setQrCode(null);
setQrError(null);
setQrInstanceName("");
// Reload instances to check if connected
loadInstances();
};
const handleDisconnectClick = (instanceName: string) => {
setDisconnectInstanceName(instanceName);
setConfirmModalOpen(true);
};
const handleDisconnectConfirm = async () => {
setDisconnectLoading(true);
try {
const response = await fetch(`/api/instances/${disconnectInstanceName}`, {
method: "DELETE",
});
if (!response.ok) {
throw new Error("Failed to disconnect instance");
}
// Success!
setToastMessage("Instância desconectada com sucesso");
setToastType("success");
setToastVisible(true);
// Close modal and reload
setConfirmModalOpen(false);
await loadInstances();
} catch (error: unknown) {
// Error
setToastMessage("Falha ao desconectar. Tente novamente.");
setToastType("error");
setToastVisible(true);
console.error("Disconnect error:", error);
} finally {
setDisconnectLoading(false);
setDisconnectInstanceName("");
}
};
const handleConnectGoogleCalendar = async () => {
try {
setCalendarLoading(true);
const {
data: { user },
} = await supabase.auth.getUser();
if (!user) {
throw new Error("User not authenticated");
}
// Chamar API route que gera oauth_url via n8n
const response = await fetch("/api/google-calendar/auth");
if (!response.ok) {
throw new Error("Failed to get OAuth URL");
}
const data = await response.json();
if (!data.oauthUrl) {
throw new Error("No OAuth URL returned");
}
// Redirecionar para Google OAuth (URL gerada pelo n8n com state correto)
window.location.href = data.oauthUrl;
} catch (error) {
console.error("Error connecting Google Calendar:", error);
setToastMessage("Erro ao conectar Google Calendar. Tente novamente.");
setToastType("error");
setToastVisible(true);
} finally {
setCalendarLoading(false);
}
};
return (
<div className="space-y-6">
{/* Welcome Section */}
<div className="bg-gray-800 rounded-lg p-6 border border-gray-700">
<h2 className="text-2xl font-bold text-white mb-2">
Bem-vindo ao Portal AutomatizaSE, {userName}!
</h2>
<p className="text-gray-400">
Gerencie suas integrações de WhatsApp e Google Calendar.
</p>
</div>
{/* WhatsApp Instances Section */}
<div>
<h3 className="text-xl font-semibold text-white mb-4">
Instâncias WhatsApp
</h3>
{loading ? (
<div className="text-center py-8 text-gray-400">
Carregando instâncias...
</div>
) : instances.length === 0 ? (
<div className="text-center py-8 text-gray-400">
Nenhuma instância configurada
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{instances.map((instance) => (
<WhatsAppInstanceCard
key={instance.instance}
instance={instance.instance}
status={instance.status}
error={instance.error}
onGenerateQR={() => handleGenerateQR(instance.instance)}
onDisconnect={() => handleDisconnectClick(instance.instance)}
/>
))}
</div>
)}
</div>
{/* Google Calendar Section */}
<div>
<h3 className="text-xl font-semibold text-white mb-4">Integrações</h3>
{calendarLoading ? (
<div className="bg-gray-800 border border-gray-700 rounded-lg p-6">
<div className="text-center py-4 text-gray-400">
Carregando status do Google Calendar...
</div>
</div>
) : (
<GoogleCalendarCard
isConnected={calendarConnected}
connectedEmail={calendarEmail}
onConnect={handleConnectGoogleCalendar}
connecting={calendarConnecting}
/>
)}
</div>
{/* QR Code Modal */}
<QRCodeModal
isOpen={qrModalOpen}
onClose={handleCloseQrModal}
qrCode={qrCode}
instanceName={qrInstanceName}
loading={qrLoading}
error={qrError}
/>
{/* Disconnect Confirmation Modal */}
<ConfirmModal
isOpen={confirmModalOpen}
onClose={() => setConfirmModalOpen(false)}
onConfirm={handleDisconnectConfirm}
title="Desconectar Instância"
message={`Tem certeza que deseja desconectar a instância "${disconnectInstanceName}"?`}
confirmText="Desconectar"
cancelText="Cancelar"
loading={disconnectLoading}
/>
{/* Toast Notification */}
<Toast
message={toastMessage}
type={toastType}
isVisible={toastVisible}
onClose={() => setToastVisible(false)}
/>
</div>
);
}