import React, { useState, useEffect, useRef } from 'react'; import { motion, AnimatePresence } from 'framer-motion'; import { supabase } from '../supabaseClient'; import ApplicantLayout from '../components/ApplicantLayout'; import PlaceholderContent from '../components/PlaceholderContent'; import { ChatIcon, SearchIcon } from '../components/Icons'; // --- Inline UI Components for Chat --- const SendIcon = () => ( ); const Avatar = ({ name }) => (
{name ? name[0].toUpperCase() : 'U'}
); const glassStyle = { background: 'rgba(15, 23, 42, 0.6)', backdropFilter: 'blur(16px)', border: '1px solid rgba(255, 255, 255, 0.08)', borderRadius: '1.25rem', display: 'flex', flexDirection: 'column', overflow: 'hidden' }; const inputStyle = { width: '100%', padding: '0.8rem', background: 'transparent', border: 'none', color: 'white', outline: 'none', fontSize: '0.95rem' }; export default function ApplicantMessages({ onNavigate }) { const [threads, setThreads] = useState([]); const [selected, setSelected] = useState(null); const [text, setText] = useState(''); const [search, setSearch] = useState(''); const [userId, setUserId] = useState(null); const [loading, setLoading] = useState(true); const scrollRef = useRef(null); // Auto-scroll to bottom of conversation useEffect(() => { scrollRef.current?.scrollIntoView({ behavior: 'smooth' }); }, [selected?.msgs]); // ✅ FETCH MESSAGES AND NAMES const fetchMsgs = async (uid) => { try { console.log("Fetching messages for user:", uid); const { data: messages, error } = await supabase .from('messages') .select('*') .or(`receiver_id.eq.${uid},sender_id.eq.${uid}`) .order('created_at', { ascending: true }); if (error) throw error; const threadMap = {}; messages.forEach(m => { let isMe = m.sender_id === uid; let otherId = isMe ? m.receiver_id : m.sender_id; // --- FIX FOR AUTOMATED MESSAGES --- // If the applicant sent it to themselves, it's a thread used for system messages. if (m.sender_id === m.receiver_id && m.sender_id === uid) { otherId = uid; // Keep a valid UUID so replies work // Only force the automated welcome message to the left side if (m.content && m.content.startsWith("Hello, Thank you for applying")) { isMe = false; } else { isMe = true; // Applicant's manual replies stay on the right } } if (!threadMap[otherId]) { threadMap[otherId] = { id: otherId, name: 'Admin / HR', subj: 'Application Update', last: '', unread: false, time: m.created_at, msgs: [], companyName: '', companyLogo: '', companyId: null }; } threadMap[otherId].msgs.push({ id: m.id, text: m.content, time: new Date(m.created_at).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }), rawTime: m.created_at, isMe }); threadMap[otherId].last = m.content; threadMap[otherId].time = m.created_at; if (!isMe && !m.is_read) threadMap[otherId].unread = true; }); const threadList = Object.values(threadMap).sort((a, b) => new Date(b.time) - new Date(a.time)); // ✅ Fetch Admin Names and Company Info from 'user_roles' table const otherUserIds = threadList.map(t => t.id); if (otherUserIds.length > 0) { const { data: rolesData } = await supabase .from('user_roles') .select('user_id, name, company_id') .in('user_id', otherUserIds); console.log("Roles Data:", rolesData); if (rolesData && rolesData.length > 0) { rolesData.forEach(role => { const thread = threadList.find(t => t.id === role.user_id); if (thread && role.name) thread.name = role.name; if (thread) thread.companyId = role.company_id; }); // ✅ Fetch Company Details including logo from storage const companyIds = [...new Set(rolesData.map(r => r.company_id).filter(Boolean))]; console.log("Company IDs to fetch:", companyIds); if (companyIds.length > 0) { const { data: companiesData } = await supabase .from('companies') .select('id, name, logo_url') .in('id', companyIds); console.log("Companies Data:", companiesData); if (companiesData) { companiesData.forEach(company => { threadList.forEach(thread => { if (thread.companyId === company.id) { thread.companyName = company.name; thread.companyLogo = company.logo_url; console.log(`Set company for thread ${thread.id}:`, company.name, company.logo_url); } }); }); } } } else { console.log("No roles data found or rolesData is empty"); } } console.log("Final Thread List:", threadList); setThreads(threadList); // Refresh currently open chat window if data changed if (selected) { const updated = threadList.find(t => t.id === selected.id); if (updated) setSelected(updated); } } catch (err) { console.error("Message Fetch Error:", err); } finally { setLoading(false); } }; // ✅ REAL-TIME LISTENER useEffect(() => { let channel; const init = async () => { const { data: { user } } = await supabase.auth.getUser(); if (!user) return; setUserId(user.id); await fetchMsgs(user.id); // Create a channel to listen for any new messages where THIS user is the receiver channel = supabase.channel('applicant_inbox') .on('postgres_changes', { event: 'INSERT', schema: 'public', table: 'messages', filter: `receiver_id=eq.${user.id}` }, () => { console.log("New message received from Admin!"); fetchMsgs(user.id); }) .subscribe(); }; init(); return () => { if (channel) supabase.removeChannel(channel); }; }, []); const markAsRead = async (t) => { setSelected(t); if (t.unread && userId) { // Optimistic UI Update setThreads(threads.map(x => x.id === t.id ? { ...x, unread: false } : x)); // Database Update await supabase.from('messages') .update({ is_read: true }) .eq('receiver_id', userId) .eq('sender_id', t.id) .eq('is_read', false); } }; const sendMsg = async () => { if (!text.trim() || !selected || !userId) return; const messageText = text.trim(); setText(''); // Send to Supabase const { error } = await supabase.from('messages').insert([{ sender_id: userId, receiver_id: selected.id, content: messageText, is_read: false }]); if (error) { console.error("Send Error:", error); } else { fetchMsgs(userId); // Refresh to show my sent message } }; const unsendMsg = async (msgId) => { if (!window.confirm("Unsend this message?")) return; // 1. Try to delete the message const { data, error } = await supabase.from('messages').delete().eq('id', msgId).select(); if (error) { console.error("Unsend Error:", error); alert("Failed to unsend message."); return; } // 2. If RLS silently blocks deletion (data is empty), fallback to updating the content if (!data || data.length === 0) { const { error: updateError } = await supabase .from('messages') .update({ content: "🚫 This message was unsent" }) .eq('id', msgId); if (updateError) { alert("Database policies prevent unsending this message."); return; } } fetchMsgs(userId); }; const canUnsend = (rawTime) => { if (!rawTime) return false; const msgTime = new Date(rawTime).getTime(); return (Date.now() - msgTime) < 5 * 60 * 1000; // 5 minutes }; const filtered = threads.filter(t => t.name.toLowerCase().includes(search.toLowerCase())); return (
{/* --- CONVERSATIONS LIST --- */}

Inbox

setSearch(e.target.value)} placeholder="Search..." style={{ ...inputStyle, paddingLeft: '2.5rem' }} />
{loading ? (
Loading...
) : filtered.length === 0 ? (
No messages found.
) : ( filtered.map(t => { const isAct = selected?.id === t.id; return ( markAsRead(t)} whileHover={{ scale: 0.98 }} style={{ padding: '1.25rem', marginBottom: '0.5rem', borderRadius: '1rem', cursor: 'pointer', position: 'relative', background: isAct ? 'rgba(251, 191, 36, 0.08)' : 'transparent', border: isAct ? '1px solid rgba(251,191,36,0.3)' : '1px solid transparent', display: 'flex', gap: '1rem' }} > {t.unread && !isAct &&
} {/* Company Logo */} {t.companyLogo ? ( {t.companyName} ) : ( )} {/* Message Info */}
{t.companyName || t.name} HR {new Date(t.time).toLocaleDateString([], { month: 'short', day: 'numeric' })}
{t.subj}
{t.last}
); }) )}
{/* --- ACTIVE CONVERSATION --- */}
{selected ? ( <>
{selected.companyLogo ? ( {selected.companyName} ) : ( )}

{selected.companyName || selected.name}

HR

Active Now

{selected.msgs.map(m => (
{m.text}
{m.time}
{m.isMe && canUnsend(m.rawTime) && ( )}
))}