"use client"; import React, { useState, useEffect } from "react"; import { supabase } from "@/lib/supabase"; import { useApp } from "@/components/AppContext"; import ReactMarkdown from "react-markdown"; import remarkGfm from "remark-gfm"; interface Comment { id: string; text: string; timestamp: string; // ISO string author?: string; stage?: string; isAi?: boolean; } interface Interview { id: string; candidate_id: string; job_id: string; interview_date: string; stage: string; feedback: string | null; created_at: string; updated_at: string; pinned: boolean; candidates: { id: string; name: string; contact_info: { email: string; phone: string; skills?: string[]; summary?: string; }; } | null; jobs: { id: string; title: string; requirements: { text?: string; }; } | null; } interface Job { id: string; title: string; } const translations = { en: { interviewsTitle: "Interviews", interviewsSubtitle: "Manage scheduled candidate interview stages and write evaluation feedback.", loadingInterviews: "Loading interviews...", noInterviews: "No interviews scheduled. Upload candidate CVs under the Jobs tab to trigger evaluations.", unknownCandidate: "Unknown Candidate", unknownJob: "Unknown Job", role: "Role", dateScheduled: "Date Scheduled", stage: "Stage", interviewFeedback: "Interview Feedback & Timeline", feedbackPlaceholder: "Write assessment feedback, observations or notes...", saving: "Saving...", updateSuccess: "Interview updated successfully!", updateFailed: "Failed to update interview.", screening: "Screening", technical: "Technical", cultural: "Cultural", offer: "Offer", hired: "Hired", rejected: "Rejected", allPositions: "All Positions", openPositions: "Open Positions", addComment: "Add Comment", commentPlaceholder: "Type a new comment/update...", noCommentsYet: "No comments added yet.", backToMain: "Back to All Interviews", selectCandidateDetails: "Select a candidate to view details", pinCandidate: "Pin Candidate", unpinCandidate: "Unpin Candidate", postedByAgent: "Agent", noCandidatesInStage: "No candidates for this position.", candidates: "Candidates", aiSuggestButton: "Get AI Suggestion", suggesting: "Analyzing...", cooldownMessage: "Cooldown: change stage or wait {hours}h to query AI suggestion again" }, es: { interviewsTitle: "Entrevistas", interviewsSubtitle: "Gestione las etapas de entrevistas programadas de los candidatos y escriba comentarios de evaluación.", loadingInterviews: "Cargando entrevistas...", noInterviews: "No hay entrevistas programadas. Cargue los CV de los candidatos en la pestaña Vacantes para activar las evaluaciones.", unknownCandidate: "Candidato Desconocido", unknownJob: "Puesto Desconocido", role: "Puesto", dateScheduled: "Fecha Programada", stage: "Etapa", interviewFeedback: "Comentarios y Cronología", feedbackPlaceholder: "Escriba comentarios, observaciones o notas de la evaluación...", saving: "Guardando...", updateSuccess: "¡Entrevista actualizada con éxito!", updateFailed: "Error al actualizar la entrevista.", screening: "Preselección", technical: "Técnica", cultural: "Cultural", offer: "Oferta", hired: "Contratado", rejected: "Rechazado", allPositions: "Todos los Puestos", openPositions: "Puestos Abiertos", addComment: "Agregar Comentario", commentPlaceholder: "Escriba un nuevo comentario o actualización...", noCommentsYet: "Aún no hay comentarios agregados.", backToMain: "Volver a Todas las Entrevistas", selectCandidateDetails: "Seleccione un candidato para ver los detalles", pinCandidate: "Fijar Candidato", unpinCandidate: "Desfijar Candidato", postedByAgent: "Agente", noCandidatesInStage: "No hay candidatos para este puesto.", candidates: "Candidatos", aiSuggestButton: "Obtener Sugerencia IA", suggesting: "Analizando...", cooldownMessage: "Cooldown: cambie la etapa o espere {hours}h para volver a pedir sugerencia" } }; const translateStage = (stage: string, lang: "en" | "es") => { if (lang === "es") { if (stage === "Screening") return "Preselección"; if (stage === "Technical") return "Técnica"; if (stage === "Cultural") return "Cultural"; if (stage === "Offer") return "Oferta"; if (stage === "Hired") return "Contratado"; if (stage === "Rejected") return "Rechazado"; } return stage; }; function parseFeedback(feedbackText: string | null): Comment[] { if (!feedbackText) return []; try { const parsed = JSON.parse(feedbackText); if (Array.isArray(parsed)) { return parsed as Comment[]; } } catch { // Ignore and fall back to plain text format } return [{ id: "legacy-initial", text: feedbackText, timestamp: new Date().toISOString() }]; } function generateCommentId(): string { return Math.random().toString(36).substring(2, 9) + Date.now().toString(36); } export default function InterviewsPage() { const { lang } = useApp(); const t = translations[lang]; const [interviews, setInterviews] = useState([]); const [jobs, setJobs] = useState([]); const [loading, setLoading] = useState(true); const [selectedJobId, setSelectedJobId] = useState(null); const [selectedInterviewId, setSelectedInterviewId] = useState(null); const [newComment, setNewComment] = useState(""); const [collapsedComments, setCollapsedComments] = useState>({}); const [suggesting, setSuggesting] = useState(false); useEffect(() => { let active = true; async function loadData() { try { const [interviewsRes, jobsRes] = await Promise.all([ supabase .from("interviews") .select("*, candidates(*), jobs(*)"), supabase .from("jobs") .select("id, title") .order("title", { ascending: true }) ]); if (interviewsRes.error) throw interviewsRes.error; if (jobsRes.error) throw jobsRes.error; if (active) { setInterviews((interviewsRes.data as unknown as Interview[]) || []); setJobs((jobsRes.data as Job[]) || []); setLoading(false); } } catch (err) { console.error("Error loading data:", err); if (active) { setLoading(false); } } } loadData(); return () => { active = false; }; }, []); const updateInterviewField = async (id: string, updates: Partial) => { try { const updated_at = new Date().toISOString(); const { error } = await supabase .from("interviews") .update({ ...updates, updated_at, }) .eq("id", id); if (error) throw error; // Update state locally setInterviews((prev) => prev.map((item) => item.id === id ? { ...item, ...updates, updated_at } : item ) ); } catch (err) { console.error("Error updating interview:", err); } }; const handleSelectJob = (jobId: string | null) => { setSelectedJobId(jobId); if (jobId) { const activeInt = interviews.find((i) => i.id === selectedInterviewId); if (activeInt && activeInt.job_id !== jobId) { setSelectedInterviewId(null); } } }; const togglePin = async (id: string, currentPinned: boolean) => { await updateInterviewField(id, { pinned: !currentPinned }); }; const handleAddComment = async () => { if (!selectedInterviewId || !newComment.trim()) return; const currentInterview = interviews.find((i) => i.id === selectedInterviewId); if (!currentInterview) return; const existingComments = parseFeedback(currentInterview.feedback); const commentId = generateCommentId(); const newCommentItem: Comment = { id: commentId, text: newComment.trim(), timestamp: new Date().toISOString(), author: lang === "es" ? "Agente" : "Agent" }; const updatedComments = [...existingComments, newCommentItem]; await updateInterviewField(selectedInterviewId, { feedback: JSON.stringify(updatedComments) }); setNewComment(""); }; const toggleCommentCollapse = (commentId: string) => { setCollapsedComments((prev) => ({ ...prev, [commentId]: !prev[commentId], })); }; const getInterviewCountForJob = (jobId: string) => { return interviews.filter((item) => item.job_id === jobId).length; }; const getSortedInterviews = (items: Interview[]) => { return [...items].sort((a, b) => { if (a.pinned && !b.pinned) return -1; if (!a.pinned && b.pinned) return 1; if (a.pinned && b.pinned) { const nameA = a.candidates?.name || ""; const nameB = b.candidates?.name || ""; return nameA.localeCompare(nameB); } const dateA = new Date(a.updated_at || a.created_at).getTime(); const dateB = new Date(b.updated_at || b.created_at).getTime(); return dateA - dateB; }); }; const getFilteredInterviews = () => { if (selectedJobId) { return interviews.filter((item) => item.job_id === selectedJobId); } return interviews; }; const filteredInterviews = getFilteredInterviews(); const sortedInterviews = getSortedInterviews(filteredInterviews); const selectedInterview = interviews.find((item) => item.id === selectedInterviewId) || null; const selectedInterviewComments = selectedInterview ? parseFeedback(selectedInterview.feedback) : []; // Cooldown calculation for AI suggestion const lastAiComment = [...selectedInterviewComments] .reverse() .find((c) => c.author === "AI Assistant" || c.author === "Asistente IA" || c.isAi); const getCooldownStatus = () => { if (!selectedInterview || !lastAiComment) return { active: false, hours: 0 }; if (lastAiComment.stage && lastAiComment.stage !== selectedInterview.stage) return { active: false, hours: 0 }; const lastTime = new Date(lastAiComment.timestamp).getTime(); const oneDayMs = 24 * 60 * 60 * 1000; // eslint-disable-next-line react-hooks/purity const elapsed = Date.now() - lastTime; if (elapsed < oneDayMs) { return { active: true, hours: Math.ceil((oneDayMs - elapsed) / (1000 * 60 * 60)) }; } return { active: false, hours: 0 }; }; const cooldownStatus = getCooldownStatus(); const cooldownActive = cooldownStatus.active; const cooldownHours = cooldownStatus.hours; const handleGetAiSuggestion = async () => { if (!selectedInterview || cooldownActive || suggesting) return; setSuggesting(true); try { const response = await fetch("/api/interviews/suggest", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ candidateName: selectedInterview.candidates?.name, jobTitle: selectedInterview.jobs?.title, currentStage: selectedInterview.stage, candidateSummary: selectedInterview.candidates?.contact_info?.summary, candidateSkills: selectedInterview.candidates?.contact_info?.skills, jobRequirements: selectedInterview.jobs?.requirements?.text, commentHistory: selectedInterviewComments, lang, }), }); if (!response.ok) throw new Error("Failed to get suggestion"); const data = await response.json(); // Save suggestion as comment const commentId = generateCommentId(); const newCommentItem: Comment = { id: commentId, text: data.suggestion, timestamp: new Date().toISOString(), author: lang === "es" ? "Asistente IA" : "AI Assistant", stage: selectedInterview.stage, isAi: true }; const updatedComments = [...selectedInterviewComments, newCommentItem]; await updateInterviewField(selectedInterview.id, { feedback: JSON.stringify(updatedComments) }); } catch (err) { console.error("Error getting AI suggestion:", err); } finally { setSuggesting(false); } }; return (

{t.interviewsTitle}

{t.interviewsSubtitle}

{/* Back to main button */} {(selectedJobId !== null || selectedInterviewId !== null) && ( )}
{loading ? (

{t.loadingInterviews}

) : interviews.length === 0 ? (

{t.noInterviews}

) : (
{/* COLUMN 1: Open Positions Sidebar */}

{t.openPositions}

{jobs.map((job) => { const count = getInterviewCountForJob(job.id); return ( ); })}
{/* COLUMN 2: Candidates List */}

{t.candidates}

{sortedInterviews.length === 0 ? (

{t.noCandidatesInStage}

) : (
{sortedInterviews.map((interview) => (
setSelectedInterviewId(interview.id)} className={`cursor-pointer p-4 rounded-lg shadow-sm border transition duration-200 relative ${ selectedInterviewId === interview.id ? "bg-slate-50 dark:bg-slate-800/40 border-blue-300 dark:border-blue-800" : "bg-white dark:bg-slate-900 border-slate-200 dark:border-slate-800 hover:border-slate-300 dark:hover:border-slate-700" } ${ interview.pinned ? "border-l-4 border-l-blue-600 pl-3" : "border-l border-l-slate-200 dark:border-l-slate-800 pl-4" }`} >

{interview.candidates?.name || t.unknownCandidate}

{interview.jobs?.title || t.unknownJob}

{translateStage(interview.stage, lang)}
))}
)}
{/* COLUMN 3: Candidate Details Pane */}
{!selectedInterview ? (

{t.selectCandidateDetails}

) : (
{/* Details Header */}

{selectedInterview.candidates?.name || t.unknownCandidate}

{t.role}: {selectedInterview.jobs?.title || t.unknownJob} | {t.dateScheduled}: {new Date(selectedInterview.interview_date).toLocaleString()}
{/* Stage Dropdown Selector */}
{/* Comments Thread System */}

{t.interviewFeedback}

{/* AI Suggestion Button */}
{/* Comment List */}
{selectedInterviewComments.length === 0 ? (

{t.noCommentsYet}

) : ( selectedInterviewComments.map((comment) => { const isCollapsed = collapsedComments[comment.id] || false; const isAiComment = comment.isAi || comment.author === "AI Assistant" || comment.author === "Asistente IA"; return (
toggleCommentCollapse(comment.id)} className={`p-3 rounded-lg border flex flex-col gap-1.5 transition-colors cursor-pointer select-none ${ isAiComment ? "bg-blue-50/20 dark:bg-blue-950/10 border-blue-100 dark:border-blue-900/50 hover:bg-blue-50/30 dark:hover:bg-blue-950/20" : "bg-slate-50 dark:bg-slate-800/40 border-slate-100 dark:border-slate-800/60 hover:bg-slate-100/40 dark:hover:bg-slate-800/50" }`} >
{comment.author || t.postedByAgent}
{new Date(comment.timestamp).toLocaleString()}
{isCollapsed ? ( ) : ( )}
{!isCollapsed && (
{comment.text}
)}
); }) )}
{/* Add Comment Input */}