fix: Add graceful handling for unconfigured Appwrite in messages page
When Appwrite is not configured: - Shows friendly 'Service not configured' message - Displays icon and helpful description - Links to dashboard and settings - Shows technical details in collapsible section - Prevents client-side crash
This commit is contained in:
parent
bf41570cae
commit
7d797aac2b
1 changed files with 148 additions and 66 deletions
|
|
@ -7,11 +7,19 @@ import { Card, CardDescription, CardHeader, CardTitle } from "@/components/ui/ca
|
||||||
import { Badge } from "@/components/ui/badge"
|
import { Badge } from "@/components/ui/badge"
|
||||||
import { Textarea } from "@/components/ui/textarea"
|
import { Textarea } from "@/components/ui/textarea"
|
||||||
import { ScrollArea } from "@/components/ui/scroll-area"
|
import { ScrollArea } from "@/components/ui/scroll-area"
|
||||||
import { Search, Send, Paperclip } from "lucide-react"
|
import { Search, Send, Paperclip, MessageSquareOff, Settings } from "lucide-react"
|
||||||
import { chatApi, Conversation, Message } from "@/lib/api"
|
import { chatApi, Conversation, Message } from "@/lib/api"
|
||||||
import { appwriteClient, APPWRITE_CONFIG } from "@/lib/appwrite"
|
import { appwriteClient, APPWRITE_CONFIG } from "@/lib/appwrite"
|
||||||
import { toast } from "sonner"
|
import { toast } from "sonner"
|
||||||
import { formatDistanceToNow } from "date-fns"
|
import { formatDistanceToNow } from "date-fns"
|
||||||
|
import Link from "next/link"
|
||||||
|
|
||||||
|
// Check if Appwrite is properly configured
|
||||||
|
const isAppwriteConfigured = () => {
|
||||||
|
const projectId = process.env.NEXT_PUBLIC_APPWRITE_PROJECT_ID
|
||||||
|
const databaseId = process.env.NEXT_PUBLIC_APPWRITE_DATABASE_ID
|
||||||
|
return !!(projectId && projectId.trim() !== '' && databaseId && databaseId.trim() !== '')
|
||||||
|
}
|
||||||
|
|
||||||
export default function AdminMessagesPage() {
|
export default function AdminMessagesPage() {
|
||||||
const [searchTerm, setSearchTerm] = useState("")
|
const [searchTerm, setSearchTerm] = useState("")
|
||||||
|
|
@ -20,11 +28,26 @@ export default function AdminMessagesPage() {
|
||||||
const [messages, setMessages] = useState<Message[]>([])
|
const [messages, setMessages] = useState<Message[]>([])
|
||||||
const [messageText, setMessageText] = useState("")
|
const [messageText, setMessageText] = useState("")
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
|
const [serviceConfigured, setServiceConfigured] = useState(true)
|
||||||
|
const [error, setError] = useState<string | null>(null)
|
||||||
|
|
||||||
const processedMessageIds = useRef(new Set<string>())
|
const processedMessageIds = useRef(new Set<string>())
|
||||||
|
|
||||||
|
// Check configuration on mount
|
||||||
|
useEffect(() => {
|
||||||
|
setServiceConfigured(isAppwriteConfigured())
|
||||||
|
if (!isAppwriteConfigured()) {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
// Fetch Conversations
|
// Fetch Conversations
|
||||||
const fetchConversations = async () => {
|
const fetchConversations = async () => {
|
||||||
|
if (!serviceConfigured) {
|
||||||
|
setLoading(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const data = await chatApi.listConversations()
|
const data = await chatApi.listConversations()
|
||||||
setConversations(data)
|
setConversations(data)
|
||||||
|
|
@ -32,20 +55,28 @@ export default function AdminMessagesPage() {
|
||||||
setSelectedConversation(data[0])
|
setSelectedConversation(data[0])
|
||||||
}
|
}
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
} catch (error) {
|
setError(null)
|
||||||
|
} catch (error: any) {
|
||||||
console.error("Failed to load conversations", error)
|
console.error("Failed to load conversations", error)
|
||||||
toast.error("Failed to load conversations")
|
// Check if it's a configuration error
|
||||||
|
if (error?.message?.includes('Project ID') || error?.code === 400) {
|
||||||
|
setServiceConfigured(false)
|
||||||
|
} else {
|
||||||
|
setError("Falha ao carregar conversas. Tente novamente.")
|
||||||
|
}
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (serviceConfigured) {
|
||||||
fetchConversations()
|
fetchConversations()
|
||||||
}, [])
|
}
|
||||||
|
}, [serviceConfigured])
|
||||||
|
|
||||||
// Fetch Messages when conversation changes
|
// Fetch Messages when conversation changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!selectedConversation) return
|
if (!selectedConversation || !serviceConfigured) return
|
||||||
|
|
||||||
setMessages([])
|
setMessages([])
|
||||||
processedMessageIds.current.clear()
|
processedMessageIds.current.clear()
|
||||||
|
|
@ -65,7 +96,8 @@ export default function AdminMessagesPage() {
|
||||||
|
|
||||||
// Appwrite Realtime Subscription
|
// Appwrite Realtime Subscription
|
||||||
// Only subscribe if config is present
|
// Only subscribe if config is present
|
||||||
if (APPWRITE_CONFIG.databaseId && APPWRITE_CONFIG.collectionId) {
|
if (APPWRITE_CONFIG.databaseId && APPWRITE_CONFIG.collectionId && serviceConfigured) {
|
||||||
|
try {
|
||||||
const channel = `databases.${APPWRITE_CONFIG.databaseId}.collections.${APPWRITE_CONFIG.collectionId}.documents`
|
const channel = `databases.${APPWRITE_CONFIG.databaseId}.collections.${APPWRITE_CONFIG.collectionId}.documents`
|
||||||
const unsubscribe = appwriteClient.subscribe(channel, (response) => {
|
const unsubscribe = appwriteClient.subscribe(channel, (response) => {
|
||||||
if (response.events.includes("databases.*.collections.*.documents.*.create")) {
|
if (response.events.includes("databases.*.collections.*.documents.*.create")) {
|
||||||
|
|
@ -73,25 +105,16 @@ export default function AdminMessagesPage() {
|
||||||
|
|
||||||
// Check if belongs to current conversation
|
// Check if belongs to current conversation
|
||||||
if (payload.conversation_id === selectedConversation.id) {
|
if (payload.conversation_id === selectedConversation.id) {
|
||||||
// Check if we already have it (deduplication)
|
|
||||||
// The Payload ID is likely the Appwrite Document ID, which usually matches our Message ID if we set it.
|
|
||||||
// If backend sets ID, it matches.
|
|
||||||
const msgId = payload.$id
|
const msgId = payload.$id
|
||||||
if (processedMessageIds.current.has(msgId)) return
|
if (processedMessageIds.current.has(msgId)) return
|
||||||
|
|
||||||
// We don't know "isMine" here easily without user ID.
|
|
||||||
// But if WE sent it, we likely added it optimistically or via API response which adds to processedIds.
|
|
||||||
// So assume incoming realtime events are from OTHERS?
|
|
||||||
// Not necessarily, Realtime echoes back my own messages too.
|
|
||||||
// Since I adding to processedIds on Send, I should filter my own echoes.
|
|
||||||
|
|
||||||
const newMessage: Message = {
|
const newMessage: Message = {
|
||||||
id: msgId,
|
id: msgId,
|
||||||
conversationId: payload.conversation_id,
|
conversationId: payload.conversation_id,
|
||||||
senderId: payload.sender_id,
|
senderId: payload.sender_id,
|
||||||
content: payload.content,
|
content: payload.content,
|
||||||
createdAt: payload.timestamp,
|
createdAt: payload.timestamp,
|
||||||
isMine: false // Default to false, assuming 'mine' are handled by UI state update on send
|
isMine: false
|
||||||
}
|
}
|
||||||
|
|
||||||
setMessages(prev => [...prev, newMessage])
|
setMessages(prev => [...prev, newMessage])
|
||||||
|
|
@ -100,8 +123,11 @@ export default function AdminMessagesPage() {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
return () => unsubscribe()
|
return () => unsubscribe()
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to subscribe to realtime", error)
|
||||||
}
|
}
|
||||||
}, [selectedConversation])
|
}
|
||||||
|
}, [selectedConversation, serviceConfigured])
|
||||||
|
|
||||||
const filteredConversations = conversations.filter((conv) =>
|
const filteredConversations = conversations.filter((conv) =>
|
||||||
(conv.participantName || "Unknown").toLowerCase().includes(searchTerm.toLowerCase()),
|
(conv.participantName || "Unknown").toLowerCase().includes(searchTerm.toLowerCase()),
|
||||||
|
|
@ -118,7 +144,7 @@ export default function AdminMessagesPage() {
|
||||||
|
|
||||||
// Update messages list
|
// Update messages list
|
||||||
setMessages(prev => [...prev, newMsg])
|
setMessages(prev => [...prev, newMsg])
|
||||||
processedMessageIds.current.add(newMsg.id) // Mark as processed to ignore realtime echo if ID matches
|
processedMessageIds.current.add(newMsg.id)
|
||||||
|
|
||||||
// Update conversation last message
|
// Update conversation last message
|
||||||
setConversations(prev => prev.map(c =>
|
setConversations(prev => prev.map(c =>
|
||||||
|
|
@ -144,6 +170,8 @@ export default function AdminMessagesPage() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Service not configured - show friendly message
|
||||||
|
if (!serviceConfigured) {
|
||||||
return (
|
return (
|
||||||
<div className="space-y-8">
|
<div className="space-y-8">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
|
|
@ -152,7 +180,61 @@ export default function AdminMessagesPage() {
|
||||||
<p className="text-muted-foreground mt-1">Communicate with candidates and companies</p>
|
<p className="text-muted-foreground mt-1">Communicate with candidates and companies</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Stats (Calculated from conversations for now, or fetch API stats later) */}
|
{/* Not Configured Card */}
|
||||||
|
<Card className="p-8">
|
||||||
|
<div className="flex flex-col items-center justify-center text-center space-y-4">
|
||||||
|
<div className="p-4 bg-muted rounded-full">
|
||||||
|
<MessageSquareOff className="h-12 w-12 text-muted-foreground" />
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<h2 className="text-xl font-semibold">Serviço de Mensagens não Configurado</h2>
|
||||||
|
<p className="text-muted-foreground max-w-md">
|
||||||
|
O serviço de mensagens em tempo real (Appwrite) ainda não foi configurado para este ambiente.
|
||||||
|
Entre em contato com o administrador do sistema para ativar esta funcionalidade.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-4 mt-4">
|
||||||
|
<Link href="/dashboard">
|
||||||
|
<Button variant="outline">Voltar ao Dashboard</Button>
|
||||||
|
</Link>
|
||||||
|
<Link href="/dashboard/settings">
|
||||||
|
<Button>
|
||||||
|
<Settings className="h-4 w-4 mr-2" />
|
||||||
|
Configurações
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Technical Details (collapsed by default) */}
|
||||||
|
<details className="mt-6 text-left w-full max-w-md">
|
||||||
|
<summary className="text-sm text-muted-foreground cursor-pointer hover:text-foreground">
|
||||||
|
Detalhes técnicos
|
||||||
|
</summary>
|
||||||
|
<div className="mt-2 p-4 bg-muted rounded-lg text-sm font-mono">
|
||||||
|
<p>Variáveis necessárias:</p>
|
||||||
|
<ul className="list-disc list-inside mt-2 space-y-1 text-muted-foreground">
|
||||||
|
<li>NEXT_PUBLIC_APPWRITE_ENDPOINT</li>
|
||||||
|
<li>NEXT_PUBLIC_APPWRITE_PROJECT_ID</li>
|
||||||
|
<li>NEXT_PUBLIC_APPWRITE_DATABASE_ID</li>
|
||||||
|
<li>NEXT_PUBLIC_APPWRITE_COLLECTION_ID</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-8">
|
||||||
|
{/* Header */}
|
||||||
|
<div>
|
||||||
|
<h1 className="text-3xl font-bold text-foreground">Messages</h1>
|
||||||
|
<p className="text-muted-foreground mt-1">Communicate with candidates and companies</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Stats */}
|
||||||
<div className="grid gap-4 md:grid-cols-4">
|
<div className="grid gap-4 md:grid-cols-4">
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader className="pb-3">
|
<CardHeader className="pb-3">
|
||||||
|
|
@ -168,7 +250,6 @@ export default function AdminMessagesPage() {
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
</Card>
|
</Card>
|
||||||
{/* Placeholders */}
|
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader className="pb-3">
|
<CardHeader className="pb-3">
|
||||||
<CardDescription>Replied today</CardDescription>
|
<CardDescription>Replied today</CardDescription>
|
||||||
|
|
@ -202,6 +283,7 @@ export default function AdminMessagesPage() {
|
||||||
<ScrollArea className="h-[calc(600px-80px)]">
|
<ScrollArea className="h-[calc(600px-80px)]">
|
||||||
<div className="p-2">
|
<div className="p-2">
|
||||||
{loading ? <p className="p-4 text-center text-muted-foreground">Loading...</p> :
|
{loading ? <p className="p-4 text-center text-muted-foreground">Loading...</p> :
|
||||||
|
error ? <p className="p-4 text-center text-destructive">{error}</p> :
|
||||||
filteredConversations.length === 0 ? <p className="p-4 text-center text-muted-foreground">No conversations found</p> :
|
filteredConversations.length === 0 ? <p className="p-4 text-center text-muted-foreground">No conversations found</p> :
|
||||||
filteredConversations.map((conversation) => (
|
filteredConversations.map((conversation) => (
|
||||||
<button
|
<button
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue