// pages/evaluation.tsx "use client"; import { useState, useEffect } from "react"; import { useParams, useRouter } from "next/navigation"; import { axiosInstance } from "@/services/axios"; // Use @hello-pangea/dnd for drag-and-drop. import { DragDropContext, Droppable, Draggable } from "@hello-pangea/dnd"; // Import shadcn/ui components (adjust imports as needed) import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; // ---------------------------------------------------------------- // Import Chart.js and react-chartjs-2 for the final analysis chart. // ---------------------------------------------------------------- import { Chart as ChartJS, CategoryScale, LinearScale, BarElement, Title, Tooltip, Legend, } from "chart.js"; import { Bar } from "react-chartjs-2"; ChartJS.register(CategoryScale, LinearScale, BarElement, Title, Tooltip, Legend); // ---------------------------------------------------------------- // Steps Definition – each step represents a cluster evaluation. // ---------------------------------------------------------------- const steps = [ { type: "cluster1", questionEndpoint: "/question/Intrinsic_value", submissionEndpoint: "/intrinsic", title: "System Quality", }, { type: "cluster2", questionEndpoint: "/question/Business_value", submissionEndpoint: "/business", title: "Business Evaluation", }, { type: "cluster3", questionEndpoint: "/question/Cost_value", submissionEndpoint: "/cost", title: "Cost Evaluation", }, { type: "cluster4", questionEndpoint: "/question/ASGC_value", submissionEndpoint: "/ASGC", title: "ACGS Evaluation", }, ]; // ---------------------------------------------------------------- // Score Labels and Colors // ---------------------------------------------------------------- const scoreLabels: { [key: string]: string } = { "1": "Low", "2": "Below Average", "3": "Average", "4": "Above Average", "5": "High", }; const columnBgColors: { [key: string]: string } = { "1": "bg-red-50", "2": "bg-orange-50", "3": "bg-yellow-50", "4": "bg-blue-50", "5": "bg-green-50", }; const columnHeaderColors: { [key: string]: string } = { "1": "bg-red-600", "2": "bg-orange-600", "3": "bg-yellow-600", "4": "bg-blue-600", "5": "bg-green-600", }; // ---------------------------------------------------------------- // Types for Question, UserInfo, and ClusterResponse // ---------------------------------------------------------------- interface Question { _id: string; question_no: string; question_detail: string; type: string; answer_type: string; created_at: string; updated_at: string | null; answers: { [key: string]: number }; // e.g. { "answer1": 5, ... } is_active: boolean; created_by: string; } interface UserInfo { _id: string; userId: string; type: string; name: string; org_name: string; created_at: string; updated_at: string | null; cluster: { [key: string]: boolean }; deltas: { [key: string]: number }; is_active: boolean; } interface ClusterResponse { cluster: string; title: string; answers: { [key: string]: number }; questionDetail: string; // The original answer keys (for restoring a step). answersList: { [key: string]: number }; } // ---------------------------------------------------------------- // Main EvaluationPage Component // ---------------------------------------------------------------- const EvaluationPage = () => { const { id } = useParams(); // user id from URL params const router = useRouter(); const [userInfo, setUserInfo] = useState(null); const [question, setQuestion] = useState(null); // “answers” maps each answer key to a droppable ID: // either "pool" or one of "1" to "5" const [answers, setAnswers] = useState<{ [key: string]: string }>({}); const [loading, setLoading] = useState(false); const [submitting, setSubmitting] = useState(false); // currentStep ranges from 0 to steps.length (if completed, show analysis) const [currentStep, setCurrentStep] = useState(0); // To store responses for analytics and for restoring previous steps. const [clusterResponses, setClusterResponses] = useState([]); // Flag to ensure auto-assignment runs only once per question. const [autoAssigned, setAutoAssigned] = useState(false); // State to track which cards have just been auto-assigned to trigger the pop effect. const [autoPopCards, setAutoPopCards] = useState([]); // ---------------------------------------------------------------- // 1. Fetch User Info from /userInfo?userId={id} // ---------------------------------------------------------------- useEffect(() => { if (!id) return; const fetchUserInfo = async () => { try { setLoading(true); const { data: responseData } = await axiosInstance.post(`/userInfo?userId=${id}`); if (responseData.data) { setUserInfo(responseData.data); } } catch (error) { console.error("Error fetching user info:", error); } finally { setLoading(false); } }; fetchUserInfo(); }, [id]); // ---------------------------------------------------------------- // 2. For the current step, fetch the question. // Initialize all answer cards to start in the "pool" and reset autoAssigned. // ---------------------------------------------------------------- useEffect(() => { if (!id || currentStep >= steps.length) return; const fetchQuestion = async () => { try { setLoading(true); const endpoint = steps[currentStep].questionEndpoint; const { data: questionData } = await axiosInstance.post(endpoint, {}); if (questionData && Array.isArray(questionData) && questionData.length > 0) { const fetchedQuestion = questionData[0]; setQuestion(fetchedQuestion); // All answer keys start in the pool. const initialAnswers: { [key: string]: string } = {}; Object.keys(fetchedQuestion.answers).forEach((key) => { initialAnswers[key] = "pool"; }); setAnswers(initialAnswers); setAutoAssigned(false); } } catch (error) { console.error("Error fetching question:", error); } finally { setLoading(false); } }; fetchQuestion(); }, [currentStep, id]); // ---------------------------------------------------------------- // 3. After a question loads, auto-assign the cards from the pool to // random columns with a staggered delay to animate the transition. // A pop effect is applied only during this auto-assignment. // ---------------------------------------------------------------- useEffect(() => { if ( question && !autoAssigned && Object.values(answers).every((loc) => loc === "pool") ) { const keys = Object.keys(answers); keys.forEach((key, index) => { setTimeout(() => { // Update the answer assignment. setAnswers((prev) => ({ ...prev, [key]: (Math.floor(Math.random() * 5) + 1).toString(), })); // Trigger the pop effect by adding the card to autoPopCards. setAutoPopCards((prev) => [...prev, key]); // Remove the pop effect after 300ms (duration of pop animation). setTimeout(() => { setAutoPopCards((prev) => prev.filter((id) => id !== key)); }, 300); // When the last card is processed, mark auto-assignment complete. if (index === keys.length - 1) { setAutoAssigned(true); } }, index * 300); // 300ms delay between each card }); } }, [question, autoAssigned, answers]); // ---------------------------------------------------------------- // 4. Handle Drag End – update the answers mapping. // Manual drag-drop will NOT trigger a pop animation. // Also, if the current cluster is locked (per userInfo.cluster), do nothing. // ---------------------------------------------------------------- const onDragEnd = (result: any) => { if (!result.destination) return; // If the current cluster is locked, disallow changes. if (userInfo && userInfo.cluster[steps[currentStep].type]) { return; } const { destination, source, draggableId } = result; if ( destination.droppableId === source.droppableId && destination.index === source.index ) return; setAnswers((prev) => ({ ...prev, [draggableId]: destination.droppableId, })); }; // ---------------------------------------------------------------- // 5. Allow clicking on a step in the sidebar to return to that step. // If going back, restore that step’s answers. // ---------------------------------------------------------------- const handleStepClick = (index: number) => { if (index > currentStep) return; // disallow jumping ahead if (index < currentStep) { const prevResponse = clusterResponses[index]; if (prevResponse) { setQuestion({ _id: "", question_no: "", question_detail: prevResponse.questionDetail, type: "", answer_type: "", created_at: "", updated_at: null, answers: prevResponse.answersList, is_active: true, created_by: "", }); const restoredAnswers: { [key: string]: string } = {}; Object.entries(prevResponse.answers).forEach(([key, score]) => { restoredAnswers[key] = score.toString(); }); setAnswers(restoredAnswers); } } setCurrentStep(index); }; // ---------------------------------------------------------------- // 6. When the user clicks "Next", validate that no card remains in the pool, // convert droppable IDs to numbers, post the payload, and move on. // ---------------------------------------------------------------- const handleNext = async () => { if (!id || !question) return; const unassigned = Object.values(answers).filter((val) => val === "pool"); if (unassigned.length > 0) { alert("Please assign all cards to a score column before proceeding."); return; } try { setSubmitting(true); const payloadAnswers: { [key: string]: number } = {}; Object.entries(answers).forEach(([key, value]) => { payloadAnswers[key] = parseInt(value, 10); }); const payload = { type: steps[currentStep].type, userId: id, answer: payloadAnswers, delta: 0, value: {}, }; const { data: submissionResponse } = await axiosInstance.post( steps[currentStep].submissionEndpoint, payload ); console.log("Submission response:", submissionResponse); setClusterResponses((prev) => [ ...prev, { cluster: steps[currentStep].type, title: steps[currentStep].title, answers: payloadAnswers, questionDetail: question.question_detail, answersList: question.answers, }, ]); setCurrentStep(currentStep + 1); setQuestion(null); setAnswers({}); } catch (error) { console.error("Error submitting data:", error); } finally { setSubmitting(false); } }; const SideBar = ()=>{ return(
{/* Step Navigation */}

Steps

    {steps.map((step, index) => (
  • handleStepClick(index)} className={`px-3 py-2 rounded cursor-pointer text-xs font-medium ${ index === currentStep ? "bg-blue-600 text-white" : index < currentStep ? "bg-green-500 text-white" : "bg-gray-100 text-gray-700" }`} > {step.title}
  • ))} {currentStep >= steps.length && (
  • Final Analysis
  • )}
{/* Card Pool */} {poolCards.length > 0 && (

Attributes

{(provided) => (
{poolCards.map((item, index) => ( {(provided) => (
{item.content}
)}
))} {provided.placeholder}
)}
)}
) } // ---------------------------------------------------------------- // 7. Final Analysis Screen – an elegant summary using a bar chart for delta values // and tables for each of the 4 clusters showing their answer scores. // ---------------------------------------------------------------- const FinalAnalyticsScreen = () => { const [finalUserInfo, setFinalUserInfo] = useState(null); const [chartData, setChartData] = useState(null); const [loadingFinal, setLoadingFinal] = useState(false); useEffect(() => { if (!id) return; const fetchFinalUserInfo = async () => { try { setLoadingFinal(true); const { data: responseData } = await axiosInstance.post( `/userInfo?userId=${id}` ); if (responseData.data) { setFinalUserInfo(responseData.data); const deltaKeys = Object.keys(responseData.data.deltas); const deltaValues = deltaKeys.map( (key) => responseData.data.deltas[key] ); setChartData({ labels: deltaKeys, datasets: [ { label: "Delta Values", data: deltaValues, backgroundColor: "rgba(54, 162, 235, 0.6)", borderColor: "rgba(54, 162, 235, 1)", borderWidth: 1, }, ], }); } } catch (error) { console.error("Error fetching final user info:", error); } finally { setLoadingFinal(false); } }; fetchFinalUserInfo(); }, [id]); if (loadingFinal) { return (

Loading final analysis...

); } if (!finalUserInfo) { return (

Failed to load final analysis data.

); } return (
{/* Delta Values Bar Chart */}

Delta Values

{chartData ? ( ) : (

No delta data available.

)}
{/* Tables for each of the evaluated clusters */}
{clusterResponses.map((cluster, idx) => (

{cluster.title}

{Object.entries(cluster.answers).map(([key, value]) => ( ))}
Attribute Score
{key} {value}
))}
); }; // ---------------------------------------------------------------- // 8. Loading / Error UI // ---------------------------------------------------------------- if (loading && !question) { return (

Loading...

); } if (!userInfo) { return (

User not found or invalid ID.

); } // ---------------------------------------------------------------- // 9. Prepare the lists of answer cards grouped by droppable. // ---------------------------------------------------------------- const poolCards: { id: string; content: string }[] = []; const columnCards: { [key: string]: { id: string; content: string }[] } = { "1": [], "2": [], "3": [], "4": [], "5": [], }; Object.entries(answers).forEach(([key, location]) => { if (location === "pool") { poolCards.push({ id: key, content: key }); } else { if (columnCards[location]) { columnCards[location].push({ id: key, content: key }); } } }); // If all steps have been completed, show the final analysis screen. if (currentStep >= steps.length) { return ; } // ---------------------------------------------------------------- // 10. Render the layout: a sidebar with step navigation and card pool, // and the main area with the Kanban board columns. // ---------------------------------------------------------------- return ( <>
{/* Sidebar */} {/* Main Content */}
{/* Header */}

{steps[currentStep]?.title}

Step {currentStep + 1} of {steps.length}

{question?.question_detail}

{userInfo && userInfo.cluster[steps[currentStep].type] && (

This cluster is locked for editing.

)}
{/* Kanban Board Columns */}
{(["1", "2", "3", "4", "5"] as const).map((score) => ( {(provided) => (
{/* Column Header */}

Score: {score}

{/* Column Body */}
{columnCards[score].map((item, index) => ( {(provided) => (
{item.content}

{scoreLabels[score]}

)}
))} {provided.placeholder}
)}
))}
{/* Footer */}
{currentStep < steps.length && ( )}
{/* CSS for the pop effect (applied only during auto-assignment) */} ); }; export default EvaluationPage;