import { useState, useEffect, forwardRef, useImperativeHandle } from "react"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Button } from "@/components/ui/button"; import { Textarea } from "@/components/ui/textarea"; import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, } from "@/components/ui/card"; import { Badge } from "@/components/ui/badge"; import { toast } from "sonner"; import { getRoleAssessmentQuestionsByRoleId, getAssessmentAnswersByAppraisalId, createAssessmentAnswer, updateAssessmentAnswer, getCompetencyRatingsByAppraisalId, createCompetencyRating, updateCompetencyRating, getCompetencyIndicatorsByCompetencyId, getTrainingNeedsByAppraisalId, createTrainingNeed, updateTrainingNeed, deleteTrainingNeed, getHrRemarksByAppraisalId, updateHrRemark, getAssessmentAreas, } from "@/services/appraisalApi"; import { Appraisal, Employee, AssessmentAnswer, RoleAssessmentQuestion, CompetencyRating, Competency, CompetencyIndicator, TrainingNeed, HrRemark, AssessmentArea, } from "@/types"; // Extended interface for questions with answers interface QuestionWithAnswer extends RoleAssessmentQuestion { answerId?: number; selfRemarks: string; appraiserRemarks: string; areaTitle: string; // Using areaTitle instead of areaName } interface IndicatorWithRating extends CompetencyIndicator { ratingId?: number; selfRating: number; appraiserRating: number; weightedScore: number; appraiserRemark: string; } interface CompetencyWithIndicators extends Competency { indicators: IndicatorWithRating[]; } // Helper functions for competency ratings const getRatingColorClass = (rating: number): string => { switch (rating) { case 1: return "red"; case 2: return "orange"; case 3: return "yellow"; case 4: return "green"; case 5: return "emerald"; default: return "gray"; } }; const getRatingButtonClass = (rating: number): string => { switch (rating) { case 1: return "bg-red-500 hover:bg-red-600 text-white"; case 2: return "bg-orange-500 hover:bg-orange-600 text-white"; case 3: return "bg-yellow-500 hover:bg-yellow-600 text-black"; case 4: return "bg-green-500 hover:bg-green-600 text-white"; case 5: return "bg-emerald-500 hover:bg-emerald-600 text-white"; default: return ""; } }; const getAverageRatingColorClass = (rating: number): string => { if (rating < 1.5) return "bg-red-500 text-white"; if (rating < 2.5) return "bg-orange-500 text-white"; if (rating < 3.5) return "bg-yellow-500 text-black"; if (rating < 4.5) return "bg-green-500 text-white"; return "bg-emerald-500 text-white"; }; const getRatingLabel = (rating: number): string => { switch (rating) { case 1: return "Needs significant improvement"; case 2: return "Below expectations"; case 3: return "Meets expectations"; case 4: return "Exceeds expectations"; case 5: return "Outstanding"; default: return "Not rated"; } }; const getAverageSelfRating = (competency: CompetencyWithIndicators): number => { const validRatings = competency.indicators.filter(i => i.selfRating > 0); if (validRatings.length === 0) return 0; const sum = validRatings.reduce((acc, indicator) => acc + indicator.selfRating, 0); return sum / validRatings.length; }; const getAverageManagerRating = (competency: CompetencyWithIndicators): number => { const validRatings = competency.indicators.filter(i => i.appraiserRating > 0); if (validRatings.length === 0) return 0; const sum = validRatings.reduce((acc, indicator) => acc + indicator.appraiserRating, 0); return sum / validRatings.length; }; const getTotalWeightedScore = (competency: CompetencyWithIndicators): number => { return competency.indicators.reduce((acc, indicator) => acc + indicator.weightedScore, 0); }; interface AppraisalFormProps { appraisal: Appraisal; employee: Employee; appraiser: Employee; competencies: Competency[]; isAppraiser: boolean; isHR: boolean; onSave: () => void; onSubmit: () => void; } export const AppraisalForm = forwardRef< { saveAnswers: () => Promise }, AppraisalFormProps >(({ appraisal, employee, appraiser, competencies, isAppraiser, isHR, onSave, onSubmit, }, ref) => { const [activeTab, setActiveTab] = useState("areas"); const [isLoading, setIsLoading] = useState(false); const [isSaving, setIsSaving] = useState(false); const [questionsByArea, setQuestionsByArea] = useState>({}); const [competenciesWithIndicators, setCompetenciesWithIndicators] = useState([]); const [trainingNeeds, setTrainingNeeds] = useState([]); const [newTrainingNeed, setNewTrainingNeed] = useState(""); const [hrRemarks, setHrRemarks] = useState(null); const [assessmentAreas, setAssessmentAreas] = useState>({}); // Expose methods to parent component via ref useImperativeHandle(ref, () => ({ saveAnswers })); // Fetch all necessary data on component mount useEffect(() => { const fetchAppraisalData = async () => { setIsLoading(true); try { await Promise.all([ fetchQuestions(), fetchCompetencyData(), fetchTrainingNeeds(), isHR && fetchHrRemarks(), ]); } catch (error) { console.error("Error fetching appraisal data:", error); toast.error("Failed to load appraisal data"); } finally { setIsLoading(false); } }; fetchAppraisalData(); }, [appraisal.appraisalId]); const fetchQuestions = async () => { try { // First fetch all assessment areas to get their titles const areasResponse = await getAssessmentAreas(); if (!areasResponse?.success || !areasResponse?.data) { throw new Error(areasResponse?.message || "Failed to fetch assessment areas"); } // Create a lookup map of areaId to area title const areaMap: Record = {}; areasResponse.data.forEach((area: AssessmentArea) => { areaMap[area.areaId] = area.title; }); setAssessmentAreas(areaMap); // Fetch assessment questions for the employee's role const questionsResponse = await getRoleAssessmentQuestionsByRoleId(employee.roleId); if (!questionsResponse?.success || !questionsResponse?.data) { throw new Error(questionsResponse?.message || "Failed to fetch questions"); } // Fetch existing answers for this appraisal const answersResponse = await getAssessmentAnswersByAppraisalId(appraisal.appraisalId); // Handle possible undefined data from answersResponse const answers = answersResponse?.data || []; console.log(`Fetched ${answers.length} existing answers for appraisal ${appraisal.appraisalId}`); // Map answers to questions and attach area titles const questions = questionsResponse.data.map((question) => { // Find the answer for this question, if any const answer = answers.find(a => a.questionId === question.questionId); return { ...question, answerId: answer?.answerId || undefined, // Use undefined instead of null for unanswered questions selfRemarks: answer?.selfRemarks || "", appraiserRemarks: answer?.appraiserRemarks || "", areaTitle: areaMap[question.areaId] || "Other", }; }); // Group questions by area const grouped: Record = {}; questions.forEach((question) => { const areaTitle = question.areaTitle; if (!grouped[areaTitle]) { grouped[areaTitle] = []; } grouped[areaTitle].push(question); }); setQuestionsByArea(grouped); } catch (error) { console.error("Error fetching questions:", error); throw error; } }; const fetchCompetencyData = async () => { try { console.log("Starting competency data fetch for appraisal", appraisal.appraisalId); // Fetch existing ratings for this appraisal const ratingsResponse = await getCompetencyRatingsByAppraisalId(appraisal.appraisalId); // Handle possible undefined data const ratings = ratingsResponse?.data || []; console.log(`Fetched ${ratings.length} competency ratings for appraisal ${appraisal.appraisalId}`); if (!competencies || competencies.length === 0) { console.warn("No competencies available to fetch indicators for"); setCompetenciesWithIndicators([]); return; } // For each competency, fetch its indicators const competenciesWithIndicatorsPromises = competencies.map(async (competency) => { // Add extra debugging for problematic competencies const isProblemCompetency = competency.title.includes("Client Management") || competency.title.includes("Technical Knowledge"); if (isProblemCompetency) { console.log(`Special debug for ${competency.title} (ID: ${competency.competencyId})`); } console.log(`Fetching indicators for competency ${competency.competencyId}: ${competency.title}`); const indicatorsResponse = await getCompetencyIndicatorsByCompetencyId(competency.competencyId); // Handle possible undefined data const indicators = indicatorsResponse?.data || []; if (isProblemCompetency) { console.log(`Response for ${competency.title}:`, indicatorsResponse); console.log(`Raw indicators for ${competency.title}:`, indicators); } console.log(`Fetched ${indicators.length} indicators for competency ${competency.competencyId}`); if (indicators.length === 0) { console.warn(`No indicators found for competency ${competency.competencyId}: ${competency.title}`); } // Map ratings to indicators const indicatorsWithRatings = indicators.map((indicator) => { const rating = ratings.find( (r) => r.indicatorId === indicator.indicatorId ); if (rating) { console.log(`Found existing rating for indicator ${indicator.indicatorId} (${rating.ratingId})`); } return { ...indicator, ratingId: rating?.ratingId, selfRating: rating?.selfRating || 0, appraiserRating: rating?.appraiserRating || 0, weightedScore: rating?.weightedScore || 0, appraiserRemark: rating?.appraiserRemark || "", }; }); return { ...competency, indicators: indicatorsWithRatings, }; }); const result = await Promise.all(competenciesWithIndicatorsPromises); // Check for any competencies with empty indicators result.forEach(competency => { if (competency.indicators.length === 0) { console.warn(`Competency ${competency.competencyId}: ${competency.title} has no indicators after data fetching`); } }); console.log(`Loaded ${result.length} competencies with their indicators`); setCompetenciesWithIndicators(result); // Check and fix any issues with specific competencies result.forEach(competency => { if ((competency.title.includes("Client Management") || competency.title.includes("Technical Knowledge")) && competency.indicators.length === 0) { console.warn(`Found problematic competency with no indicators: ${competency.title} (ID: ${competency.competencyId})`); } }); } catch (error) { console.error("Error fetching competency data:", error); toast.error("Failed to load competency data", { description: "Please try refreshing the page", }); throw error; } }; const fetchTrainingNeeds = async () => { try { const response = await getTrainingNeedsByAppraisalId(appraisal.appraisalId); // Handle possible undefined data const trainingNeedsData = response?.data || []; setTrainingNeeds(trainingNeedsData); } catch (error) { console.error("Error fetching training needs:", error); throw error; } }; const fetchHrRemarks = async () => { try { const response = await getHrRemarksByAppraisalId(appraisal.appraisalId); // Handle possible undefined data const hrRemarksData = response?.data || []; if (hrRemarksData.length > 0) { setHrRemarks(hrRemarksData[0]); } } catch (error) { console.error("Error fetching HR remarks:", error); throw error; } }; const handleQuestionAnswerChange = (questionId: number, field: "selfRemarks" | "appraiserRemarks", value: string) => { setQuestionsByArea((prev) => { const updated = { ...prev }; // Find the question in any area and update it Object.keys(updated).forEach((areaTitle) => { updated[areaTitle] = updated[areaTitle].map((q) => q.questionId === questionId ? { ...q, [field]: value } : q ); }); return updated; }); }; const handleCompetencyRatingChange = ( indicatorId: number, competencyId: number, field: "selfRating" | "appraiserRating" | "appraiserRemark", value: number | string ) => { setCompetenciesWithIndicators((prev) => { return prev.map((competency) => { if (competency.competencyId !== competencyId) return competency; return { ...competency, indicators: competency.indicators.map((indicator) => { if (indicator.indicatorId !== indicatorId) return indicator; // Calculate weighted score if applicable let updatedIndicator = { ...indicator, [field]: value }; if (field === "appraiserRating" && typeof value === "number") { const weightedScore = (value * competency.weightage) / 100; updatedIndicator.weightedScore = weightedScore; } return updatedIndicator; }), }; }); }); // Auto-save the rating when changed - but only for rating buttons (not comments) if (field === "selfRating" || field === "appraiserRating") { const autoSaveRating = async () => { try { // First get the latest data to avoid stale state issues const competency = competenciesWithIndicators.find(c => c.competencyId === competencyId); if (!competency) return; const indicator = competency.indicators.find(i => i.indicatorId === indicatorId); if (!indicator) return; // Update the indicator with the new value const updatedIndicator = { ...indicator, [field]: value }; // Calculate weighted score if needed if (field === "appraiserRating" && typeof value === "number") { updatedIndicator.weightedScore = (value * competency.weightage) / 100; } // Get latest ratings to check if we need to create or update const latestRatingsResponse = await getCompetencyRatingsByAppraisalId(appraisal.appraisalId); const latestRatings = latestRatingsResponse?.data || []; const existingRating = latestRatings.find(r => r.indicatorId === indicatorId); let result; if (existingRating) { // Update existing rating result = await updateCompetencyRating({ ratingId: existingRating.ratingId, appraisalId: appraisal.appraisalId, indicatorId: updatedIndicator.indicatorId, selfRating: updatedIndicator.selfRating, appraiserRating: updatedIndicator.appraiserRating, weightedScore: updatedIndicator.weightedScore, appraiserRemark: updatedIndicator.appraiserRemark, }); } else { // Create new rating result = await createCompetencyRating({ appraisalId: appraisal.appraisalId, indicatorId: updatedIndicator.indicatorId, selfRating: updatedIndicator.selfRating, appraiserRating: updatedIndicator.appraiserRating, weightedScore: updatedIndicator.weightedScore, appraiserRemark: updatedIndicator.appraiserRemark, }); } // Check if result has success property explicitly set to false const isExplicitFailure = result && result.success === false; // Check if it's a successful response - either has success=true or contains ratingId const isSuccessfulResponse = (result && result.success === true) || (result && result.data && 'ratingId' in result.data) || (result && typeof result === 'object' && 'ratingId' in result); if (isSuccessfulResponse) { console.log(`Rating ${existingRating ? 'updated' : 'created'} successfully:`, result); // Only show toast for first rating or creation to avoid spam if (!existingRating || field === "selfRating") { toast.success(`Rating ${existingRating ? 'updated' : 'saved'} successfully`); } // Refresh data to update UI with the latest ratings await refreshData(); } else if (isExplicitFailure) { console.error("Failed to save rating:", result); toast.error(`Failed to save rating: ${result.message || 'Unknown error'}`); } else { // Handle the case where result might have unexpected structure console.warn("Received unexpected response when saving rating:", result); // Check if it looks like a valid rating object if (result && typeof result === 'object' && 'indicatorId' in result && (('selfRating' in result && result.selfRating !== undefined) || ('appraiserRating' in result && result.appraiserRating !== undefined))) { console.log("Response appears to be a valid rating object, treating as success"); // Treat it as success if it has required fields if (!existingRating) { toast.success("Rating saved successfully"); } await refreshData(); } else { // Only show error if it's clearly not a success toast.error("Unable to determine if rating was saved properly"); } } } catch (error) { console.error("Error auto-saving rating:", error); // Only show toast for actual errors if (error instanceof Error) { toast.error(`Error saving rating: ${error.message}`); } else { toast.error("Error saving rating"); } } }; autoSaveRating(); } }; const handleAddTrainingNeed = async () => { if (!newTrainingNeed.trim()) return; setIsSaving(true); try { const response = await createTrainingNeed({ appraisalId: appraisal.appraisalId, needDescription: newTrainingNeed, }); if (!response.success) { throw new Error(response.message); } setTrainingNeeds([...trainingNeeds, response.data]); setNewTrainingNeed(""); toast.success("Training need added"); } catch (error) { console.error("Error adding training need:", error); toast.error("Failed to add training need"); } finally { setIsSaving(false); } }; const handleDeleteTrainingNeed = async (trainingId: number) => { setIsSaving(true); try { const response = await deleteTrainingNeed(trainingId); if (!response.success) { throw new Error(response.message); } setTrainingNeeds(trainingNeeds.filter((need) => need.trainingId !== trainingId)); toast.success("Training need deleted"); } catch (error) { console.error("Error deleting training need:", error); toast.error("Failed to delete training need"); } finally { setIsSaving(false); } }; const handleHrRemarksChange = (remarks: string) => { setHrRemarks((prev) => prev ? { ...prev, remarks } : null); }; const saveAnswers = async () => { if (isSaving) { console.log("Save operation already in progress, skipping"); return; } setIsSaving(true); console.log("Starting save operation for appraisal", appraisal.appraisalId); try { // First, fetch the latest answers to ensure we're working with up-to-date data const latestAnswersResponse = await getAssessmentAnswersByAppraisalId(appraisal.appraisalId); const latestAnswers = latestAnswersResponse?.data || []; console.log(`Found ${latestAnswers.length} existing assessment answers`); // Save all answers const saveAnswersPromises: Promise[] = []; // Tracking operations for debug logging const operations = { answerUpdates: 0, answerCreates: 0, ratingUpdates: 0, ratingCreates: 0 }; // Save assessment answers - with duplicate checking Object.values(questionsByArea).flat().forEach((question) => { // Check if this question already has an answer in the latest data const existingAnswer = latestAnswers.find(a => a.questionId === question.questionId); if (existingAnswer) { // Update existing answer operations.answerUpdates++; saveAnswersPromises.push( updateAssessmentAnswer({ answerId: existingAnswer.answerId, appraisalId: appraisal.appraisalId, questionId: question.questionId, selfRemarks: question.selfRemarks, appraiserRemarks: question.appraiserRemarks, }) ); } else if (!question.answerId) { // Only create if we're sure it doesn't exist operations.answerCreates++; saveAnswersPromises.push( createAssessmentAnswer({ appraisalId: appraisal.appraisalId, questionId: question.questionId, selfRemarks: question.selfRemarks, appraiserRemarks: question.appraiserRemarks, }) ); } }); // Similarly fetch the latest ratings const latestRatingsResponse = await getCompetencyRatingsByAppraisalId(appraisal.appraisalId); const latestRatings = latestRatingsResponse?.data || []; console.log(`Found ${latestRatings.length} existing competency ratings`); // Save competency ratings with duplicate checking competenciesWithIndicators.forEach((competency) => { competency.indicators.forEach((indicator) => { // Check if this indicator already has a rating in the latest data const existingRating = latestRatings.find(r => r.indicatorId === indicator.indicatorId); if (existingRating) { // Update existing rating operations.ratingUpdates++; saveAnswersPromises.push( updateCompetencyRating({ ratingId: existingRating.ratingId, appraisalId: appraisal.appraisalId, indicatorId: indicator.indicatorId, selfRating: indicator.selfRating, appraiserRating: indicator.appraiserRating, weightedScore: indicator.weightedScore, appraiserRemark: indicator.appraiserRemark, }) ); } else if (!indicator.ratingId) { // Only create if we're sure it doesn't exist operations.ratingCreates++; saveAnswersPromises.push( createCompetencyRating({ appraisalId: appraisal.appraisalId, indicatorId: indicator.indicatorId, selfRating: indicator.selfRating, appraiserRating: indicator.appraiserRating, weightedScore: indicator.weightedScore, appraiserRemark: indicator.appraiserRemark, }) ); } }); }); // Save HR remarks if applicable if (isHR && hrRemarks) { saveAnswersPromises.push( updateHrRemark({ hrRemarkId: hrRemarks.hrRemarkId, appraisalId: appraisal.appraisalId, remarks: hrRemarks.remarks, }) ); } console.log("Save operations:", operations); console.log(`Executing ${saveAnswersPromises.length} total API operations`); const results = await Promise.all(saveAnswersPromises); // Check if any of the operations failed const hasFailure = results.some(result => result.success === false); if (hasFailure) { console.error("One or more save operations failed", results.filter(r => !r.success)); throw new Error("One or more save operations failed"); } console.log("All operations completed successfully, refreshing data"); // Refresh data after saving to update the component state with server data await refreshData(); toast.success("Appraisal saved successfully"); console.log("Save process completed"); // Call onSave only if it's safe (won't cause infinite loops) if (typeof onSave === 'function') { onSave(); } } catch (error) { console.error("Error saving appraisal:", error); toast.error("Failed to save appraisal"); } finally { setIsSaving(false); } }; // Extract refresh logic to its own function for reuse const refreshData = async () => { try { const answersResponse = await getAssessmentAnswersByAppraisalId(appraisal.appraisalId); const answers = answersResponse?.data || []; // Update answerId for questions that were newly created setQuestionsByArea(prev => { const updated = { ...prev }; Object.keys(updated).forEach(areaTitle => { updated[areaTitle] = updated[areaTitle].map(q => { const answer = answers.find(a => a.questionId === q.questionId); if (answer) { return { ...q, answerId: answer.answerId }; } return q; }); }); return updated; }); // Refresh competency ratings const ratingsResponse = await getCompetencyRatingsByAppraisalId(appraisal.appraisalId); const ratings = ratingsResponse?.data || []; setCompetenciesWithIndicators(prev => { return prev.map(competency => { return { ...competency, indicators: competency.indicators.map(indicator => { const rating = ratings.find(r => r.indicatorId === indicator.indicatorId); if (rating) { return { ...indicator, ratingId: rating.ratingId }; } return indicator; }), }; }); }); // Refresh training needs if necessary await fetchTrainingNeeds(); // Refresh HR remarks if applicable if (isHR) { await fetchHrRemarks(); } } catch (error) { console.error("Error refreshing data:", error); } }; const handleSubmit = async () => { try { await saveAnswers(); // Data is already refreshed inside saveAnswers onSubmit(); } catch (error) { console.error("Error during submission:", error); toast.error("Failed to complete submission"); } }; // Debug: Log when competenciesWithIndicators changes useEffect(() => { console.log("competenciesWithIndicators state changed:", competenciesWithIndicators); console.log("Number of competencies with indicators:", competenciesWithIndicators.length); // Check if any competencies have zero indicators const emptyCompetencies = competenciesWithIndicators.filter(comp => !comp.indicators || comp.indicators.length === 0); if (emptyCompetencies.length > 0) { console.warn(`Found ${emptyCompetencies.length} competencies with no indicators:`, emptyCompetencies.map(c => `${c.competencyId}: ${c.title}`)); } // Check total number of indicators across all competencies const totalIndicators = competenciesWithIndicators.reduce((sum, comp) => sum + (comp.indicators?.length || 0), 0); console.log(`Total number of indicators across all competencies: ${totalIndicators}`); }, [competenciesWithIndicators]); if (isLoading) { return (
); } return (
Assessment Areas Competencies Training Needs {isHR && HR Remarks} Summary {/* Assessment Areas */}
{Object.entries(questionsByArea).map(([areaTitle, questions]) => ( {areaTitle} Provide your inputs for these assessment questions.
{questions.map((question) => (

{question.questionText}