- 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.
330 lines
10 KiB
TypeScript
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>
|
|
);
|
|
}
|