Files
pwa/src/components/assessment/AssessmentResultView.tsx
T

328 lines
13 KiB
TypeScript

import { useState } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { ChevronLeft } from 'lucide-react';
import { useNavigate } from 'react-router-dom';
import { Button } from '@/components/ui/button';
import {
DIMENSION_CONFIGS,
DIMENSION_ORDER,
type DimensionConfig,
getDescriptiveTitleForScore,
} from '@/config/resultImages';
import { DimensionCard } from './DimensionCard';
import { SmoothTextAppearance } from '@/components/motion';
import type { AssessmentSubmission } from '@/services/assessmentService';
import type { DimensionName } from '@/lib/assessment/scoring/dimensions';
import { cn } from '@/lib/utils';
import { MOTION_EASE } from '@/lib/motion';
export interface AssessmentResultViewProps {
phase: 'pre' | 'post';
currentSubmission: AssessmentSubmission;
baselineSubmission?: AssessmentSubmission | null;
onContinue?: () => void;
continueLabel?: string;
showActions?: boolean;
}
export function AssessmentResultView({
phase,
currentSubmission,
baselineSubmission,
onContinue,
continueLabel = 'مشاهده آموزشها و تمرینها',
showActions = true,
}: AssessmentResultViewProps) {
const navigate = useNavigate();
const isPre = phase === 'pre';
const scores = currentSubmission.scores;
const baselineScores = baselineSubmission?.scores;
// Active step: 0, 1, 2, 3 = dimension steps; 4 = final summary screen
const [currentStepIndex, setCurrentStepIndex] = useState<number>(0);
const [isTransitioning, setIsTransitioning] = useState<boolean>(false);
// Track revealed status for each dimension so it's preserved on back/forward
const [revealed, setRevealed] = useState<Record<DimensionName, boolean>>({
efficacy: false,
hope: false,
resilience: false,
optimism: false,
});
const totalScore = scores.total;
const maxTotalScore = 144;
const totalPercent = Math.round(
(Math.max(0, Math.min(maxTotalScore, totalScore)) / maxTotalScore) * 100
);
const hasComparison = Boolean(
!isPre && baselineScores && typeof baselineScores.total === 'number'
);
const totalDelta = hasComparison ? totalScore - (baselineScores?.total ?? 0) : 0;
const currentDimensionKey = DIMENSION_ORDER[currentStepIndex];
const isSummaryStep = currentStepIndex === 4;
const handleRevealCurrent = () => {
if (currentDimensionKey) {
setRevealed((prev) => ({ ...prev, [currentDimensionKey]: true }));
}
};
const handleNext = () => {
if (isTransitioning) return;
setIsTransitioning(true);
setCurrentStepIndex((prev) => Math.min(prev + 1, 4));
window.setTimeout(() => setIsTransitioning(false), 220);
};
const handlePrev = () => {
if (isTransitioning) return;
setIsTransitioning(true);
setCurrentStepIndex((prev) => Math.max(prev - 1, 0));
window.setTimeout(() => setIsTransitioning(false), 220);
};
const handleJumpToStep = (index: number) => {
if (isTransitioning) return;
const targetDimension = DIMENSION_ORDER[index];
if (
index < currentStepIndex ||
(index <= 3 && targetDimension && revealed[targetDimension]) ||
index === currentStepIndex
) {
setIsTransitioning(true);
setCurrentStepIndex(index);
window.setTimeout(() => setIsTransitioning(false), 220);
}
};
const handleFinalContinue = () => {
if (onContinue) {
onContinue();
} else if (isPre) {
navigate('/chat');
} else {
navigate('/course-status');
}
};
return (
<div className="mx-auto flex h-full min-h-0 w-full max-w-[430px] flex-1 flex-col" dir="rtl">
{/* 1. Low-Height, Minimal 4-Segment Progress Indicator */}
<div className="shrink-0 space-y-1.5 px-1 pb-2">
<div className="flex items-center justify-between text-xs text-muted-foreground">
<span className="font-semibold text-foreground">
{isSummaryStep ? 'جمع‌بندی نهایی' : `مؤلفه ${currentStepIndex + 1} از ۴`}
</span>
<span>
{isSummaryStep
? 'کارنامه نهایی'
: DIMENSION_CONFIGS[currentDimensionKey]?.titleFa}
</span>
</div>
{/* Clean, low-height segment bar */}
<div className="grid grid-cols-4 gap-1.5">
{DIMENSION_ORDER.map((dimKey, idx) => {
const isCompleted =
revealed[dimKey] && (currentStepIndex > idx || isSummaryStep);
const isCurrent = currentStepIndex === idx;
return (
<button
key={dimKey}
type="button"
onClick={() => handleJumpToStep(idx)}
disabled={!revealed[dimKey] && idx > currentStepIndex}
className={cn(
'h-1.5 rounded-full transition-all duration-300 focus:outline-none',
isCurrent
? 'bg-sky-500 dark:bg-sky-400'
: isCompleted
? 'bg-sky-500/50 hover:bg-sky-500 dark:bg-sky-400/50'
: 'bg-slate-200/80 dark:bg-slate-800'
)}
aria-label={DIMENSION_CONFIGS[dimKey]?.titleFa}
/>
);
})}
</div>
</div>
{/* 2. Main Content (Dimension Card OR Final Summary) */}
<div className="flex-1 min-h-0 flex flex-col">
<AnimatePresence mode="wait" initial={false}>
{!isSummaryStep && currentDimensionKey ? (
/* Step-by-Step Dimension Card */
<DimensionCard
key={currentDimensionKey}
config={DIMENSION_CONFIGS[currentDimensionKey]}
score={scores[currentDimensionKey]}
baselineScore={
hasComparison ? baselineScores?.[currentDimensionKey] : undefined
}
stepNumber={currentStepIndex + 1}
totalSteps={4}
isRevealed={revealed[currentDimensionKey]}
onReveal={handleRevealCurrent}
onNext={handleNext}
onPrev={currentStepIndex > 0 ? handlePrev : undefined}
isLastDimension={currentStepIndex === 3}
/>
) : (
/* Final Summary Screen */
<motion.div
key="final-summary"
initial={{ opacity: 0, y: 5 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -5 }}
transition={{ duration: 0.22, ease: MOTION_EASE }}
style={{ willChange: 'opacity, transform' }}
className="flex h-full min-h-0 flex-1 flex-col"
>
{/* Main Summary Glass Card */}
<div className="glass-panel flex h-full min-h-0 flex-1 flex-col justify-between overflow-y-auto space-y-4 rounded-3xl border border-white/50 p-4 sm:p-5 shadow-sm dark:border-white/10 dark:bg-slate-900/60">
{/* Header Title */}
<div className="text-center sm:text-right">
<h1 className="text-xl font-bold tracking-tight text-foreground sm:text-2xl">
{isPre
? 'کارنامه اولیه سرمایه روان‌شناختی'
: 'کارنامه نهایی دوره نویا'}
</h1>
<p className="mt-0.5 text-xs text-muted-foreground">
خلاصه وضعیت چهار مؤلفه شما
</p>
</div>
{/* Total Score Section */}
<div className="space-y-1.5 rounded-2xl bg-sky-500/10 p-4 dark:bg-sky-500/15">
<div className="flex items-baseline justify-between">
<span className="text-xs font-semibold text-sky-800 dark:text-sky-300">
نمره کل آزمون:
</span>
<div className="flex items-baseline gap-1 font-bold tabular-nums">
<span className="text-2xl font-black text-sky-900 dark:text-sky-100 sm:text-3xl">
{totalScore.toLocaleString('fa-IR')}
</span>
<span className="text-xs text-muted-foreground">از ۱۴۴</span>
</div>
</div>
<div className="h-2 w-full overflow-hidden rounded-full bg-slate-200/80 dark:bg-slate-800">
<div
className="h-full rounded-full bg-neon-gradient transition-all duration-600"
style={{ width: `${totalPercent}%` }}
/>
</div>
{hasComparison && baselineScores && (
<div className="flex items-center justify-between pt-1 text-xs text-muted-foreground">
<span>
پیش‌آزمون: {baselineScores.total.toLocaleString('fa-IR')} ➔
پس‌آزمون: {totalScore.toLocaleString('fa-IR')}
</span>
<span
className={
totalDelta > 0
? 'font-bold text-emerald-600 dark:text-emerald-400'
: totalDelta < 0
? 'font-bold text-amber-600 dark:text-amber-400'
: 'text-muted-foreground'
}
>
{totalDelta > 0
? `+${totalDelta.toLocaleString('fa-IR')} رشد کل`
: totalDelta < 0
? `${totalDelta.toLocaleString('fa-IR')} تغییر`
: 'بدون تغییر'}
</span>
</div>
)}
</div>
{/* 4-Dimension Comparative Bars (Simple & Readable) */}
<div className="space-y-2.5 pt-1">
{DIMENSION_ORDER.map((dimKey) => {
const config: DimensionConfig = DIMENSION_CONFIGS[dimKey];
const dimScore = scores[dimKey];
const dimPercent = Math.round((dimScore / 36) * 100);
return (
<div key={dimKey} className="space-y-1">
<div className="flex items-center justify-between text-xs">
<span className="font-semibold text-foreground">
{config.titleFa}
</span>
<span className="text-[11px] text-muted-foreground">
{getDescriptiveTitleForScore(dimKey, dimScore)}
</span>
</div>
<div className="h-1.5 w-full overflow-hidden rounded-full bg-slate-200/80 dark:bg-slate-800">
<div
className="h-full rounded-full transition-all duration-500"
style={{
width: `${dimPercent}%`,
backgroundColor: config.accentColor,
}}
/>
</div>
</div>
);
})}
</div>
{/* Non-diagnostic notice */}
<p className="text-center text-xs text-muted-foreground">
این نتیجه بازتاب پاسخ‌های فعلی شماست و تشخیص قطعی روان‌شناختی نیست.
</p>
{/* Mandatory Closing Text */}
<SmoothTextAppearance contentKey="final-summary-closing" distance={5} duration={0.22}>
<div className="rounded-2xl bg-white/50 p-4 text-xs leading-7 text-foreground/90 dark:bg-slate-800/40 sm:text-sm sm:leading-8">
<p className="whitespace-pre-line text-body">
حالا تصویر روشن‌تری از چهار مؤلفه سرمایه روان‌شناختی خودت داری.
{'\n\n'}
این نتیجه، نقطه شروعیه برای شناخت بهتر خودت؛ نه تعریف کامل شخصیت تو.
{'\n\n'}
برای آشنایی بیشتر با این مؤلفه‌ها و تقویت اون‌ها، پیشنهاد می‌کنیم آموزش‌های نویا رو ببینی و تمرین‌های مرتبط رو انجام بدی.
</p>
</div>
</SmoothTextAppearance>
{/* Single Prominent Button */}
{showActions && (
<div className="space-y-2 pt-1">
<Button
type="button"
variant="neon"
size="lg"
onClick={handleFinalContinue}
className="w-full gap-2 font-bold shadow-md"
>
<span>{continueLabel}</span>
<ChevronLeft className="h-4 w-4" />
</Button>
{/* Subtle Secondary Back Control */}
<div className="text-center">
<button
type="button"
onClick={() => setCurrentStepIndex(0)}
className="text-xs text-muted-foreground transition-colors hover:text-foreground"
>
مرور دوباره مؤلفه‌ها
</button>
</div>
</div>
)}
</div>
</motion.div>
)}
</AnimatePresence>
</div>
</div>
);
}