- ExecutiveSummary: Add rating badge with emoji, AI narrative section, #1 Problem/#1 Strength cards, domain complaints with progress bars - SentimentPie: Replace pie chart with card-based design showing sentiment score, emoji indicators (😊😟😐🤔), percentages - IntensityHeatmap: Transform to Praise vs Complaints heatmap with friendly domain labels (👥 Staff, 💰 Pricing, etc.) - URTBarChart: Horizontal progress bars with emojis, health indicators - TimelineChart: Add view toggles (Sentiment/Volume/Rating), trend indicator, fix chronological order (oldest→newest left→right) - ReviewIQDashboard: Streamline from 11 sections to 5, remove redundancy Removed redundant components: - DomainScores (merged into ExecutiveSummary) - KPISection (stats in header) - RatingSimulator (in ExecutiveSummary) - StrengthsWeaknesses (in ExecutiveSummary) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
239 lines
8.8 KiB
TypeScript
239 lines
8.8 KiB
TypeScript
'use client';
|
|
|
|
import { useMemo } from 'react';
|
|
import { X, Filter, TrendingUp, TrendingDown, Minus } from 'lucide-react';
|
|
import type { URTDomainPoint, URTDomain } from '../types';
|
|
import { useReviewIQFilters } from '@/contexts/ReviewIQFilterContext';
|
|
|
|
interface URTBarChartProps {
|
|
data: URTDomainPoint[];
|
|
}
|
|
|
|
// User-friendly domain config with emojis and descriptions
|
|
const DOMAIN_CONFIG: Record<string, {
|
|
emoji: string;
|
|
label: string;
|
|
color: string;
|
|
bgColor: string;
|
|
}> = {
|
|
P: { emoji: '👥', label: 'Staff & Service', color: '#3b82f6', bgColor: '#dbeafe' },
|
|
V: { emoji: '💰', label: 'Pricing & Value', color: '#ec4899', bgColor: '#fce7f3' },
|
|
J: { emoji: '⏱️', label: 'Speed & Process', color: '#8b5cf6', bgColor: '#ede9fe' },
|
|
O: { emoji: '🛍️', label: 'Product Quality', color: '#f97316', bgColor: '#ffedd5' },
|
|
A: { emoji: '📍', label: 'Availability', color: '#10b981', bgColor: '#d1fae5' },
|
|
E: { emoji: '🏢', label: 'Facilities', color: '#06b6d4', bgColor: '#cffafe' },
|
|
R: { emoji: '🤝', label: 'Trust & Ethics', color: '#f59e0b', bgColor: '#fef3c7' },
|
|
};
|
|
|
|
// Ordered domains by typical business priority
|
|
const DOMAIN_ORDER = ['P', 'V', 'J', 'O', 'A', 'E', 'R'];
|
|
|
|
/**
|
|
* Domain Distribution - Horizontal bar chart showing what customers talk about.
|
|
* User-friendly design with emojis and clear progress bars.
|
|
* Click to filter by domain.
|
|
*/
|
|
export function URTBarChart({ data }: URTBarChartProps) {
|
|
const { filters, setURTDomain } = useReviewIQFilters();
|
|
|
|
// Process and sort data
|
|
const processedData = useMemo(() => {
|
|
const lookup = new Map<string, URTDomainPoint>();
|
|
let maxCount = 0;
|
|
let totalMentions = 0;
|
|
|
|
data.forEach((d) => {
|
|
lookup.set(d.domain, d);
|
|
if (d.count > maxCount) maxCount = d.count;
|
|
totalMentions += d.count;
|
|
});
|
|
|
|
// Sort by domain order, then build rows
|
|
const rows = DOMAIN_ORDER
|
|
.filter(domain => lookup.has(domain))
|
|
.map(domain => {
|
|
const d = lookup.get(domain)!;
|
|
const config = DOMAIN_CONFIG[d.domain] || {
|
|
emoji: '📊',
|
|
label: d.domain_name || d.domain,
|
|
color: '#6b7280',
|
|
bgColor: '#f3f4f6',
|
|
};
|
|
|
|
const percentage = totalMentions > 0 ? (d.count / totalMentions) * 100 : 0;
|
|
const barWidth = maxCount > 0 ? (d.count / maxCount) * 100 : 0;
|
|
|
|
// Health indicator based on positive/negative ratio
|
|
const total = d.positive_count + d.negative_count + d.neutral_count;
|
|
const positiveRatio = total > 0 ? d.positive_count / total : 0;
|
|
const negativeRatio = total > 0 ? d.negative_count / total : 0;
|
|
|
|
let health: 'good' | 'warning' | 'critical' = 'warning';
|
|
if (positiveRatio > 0.6) health = 'good';
|
|
else if (negativeRatio > 0.5) health = 'critical';
|
|
|
|
return {
|
|
domain: d.domain,
|
|
config,
|
|
count: d.count,
|
|
reviewCount: d.review_count,
|
|
percentage,
|
|
barWidth,
|
|
health,
|
|
positiveCount: d.positive_count,
|
|
negativeCount: d.negative_count,
|
|
};
|
|
})
|
|
// Sort by count descending
|
|
.sort((a, b) => b.count - a.count);
|
|
|
|
return { rows, maxCount, totalMentions };
|
|
}, [data]);
|
|
|
|
const handleClick = (domain: string) => {
|
|
setURTDomain(filters.urtDomain === domain ? null : domain as URTDomain);
|
|
};
|
|
|
|
const isFiltering = filters.urtDomain !== null;
|
|
const hasSentimentFilter = filters.sentiment.length > 0;
|
|
|
|
return (
|
|
<div
|
|
className={`bg-white rounded-xl p-6 shadow-md transition-all ${
|
|
isFiltering
|
|
? 'border-3 border-blue-500 ring-2 ring-blue-200'
|
|
: hasSentimentFilter
|
|
? 'border-2 border-purple-400 ring-1 ring-purple-200'
|
|
: 'border-2 border-gray-300 hover:border-blue-400'
|
|
}`}
|
|
>
|
|
<div className="flex items-center justify-between mb-4">
|
|
<div>
|
|
<h3 className="text-lg font-bold text-gray-900">What Customers Talk About</h3>
|
|
<p className="text-sm text-gray-500 mt-1">
|
|
{processedData.totalMentions.toLocaleString()} total mentions
|
|
</p>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
{hasSentimentFilter && !isFiltering && (
|
|
<span className="flex items-center gap-1 text-xs text-purple-600 bg-purple-50 px-2 py-1 rounded-full">
|
|
<Filter className="w-3 h-3" />
|
|
{filters.sentiment.join(', ')}
|
|
</span>
|
|
)}
|
|
{isFiltering && (
|
|
<>
|
|
<span className="px-2 py-1 bg-blue-100 text-blue-800 text-xs font-bold rounded-full">
|
|
{DOMAIN_CONFIG[filters.urtDomain!]?.label || filters.urtDomain}
|
|
</span>
|
|
<button
|
|
onClick={() => setURTDomain(null)}
|
|
className="p-1 hover:bg-gray-200 rounded-full transition-colors"
|
|
title="Clear filter"
|
|
>
|
|
<X className="w-4 h-4 text-gray-600" />
|
|
</button>
|
|
</>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{processedData.rows.length === 0 ? (
|
|
<div className="flex items-center justify-center h-64 text-gray-500">
|
|
No data available
|
|
</div>
|
|
) : (
|
|
<div className="space-y-3">
|
|
{processedData.rows.map((row) => {
|
|
const isActive = filters.urtDomain === row.domain;
|
|
|
|
return (
|
|
<button
|
|
key={row.domain}
|
|
onClick={() => handleClick(row.domain)}
|
|
className={`w-full text-left transition-all rounded-lg p-3 ${
|
|
isActive
|
|
? 'bg-blue-50 ring-2 ring-blue-500'
|
|
: 'hover:bg-gray-50'
|
|
}`}
|
|
>
|
|
<div className="flex items-center gap-3">
|
|
{/* Emoji */}
|
|
<span className="text-2xl flex-shrink-0">{row.config.emoji}</span>
|
|
|
|
{/* Label and Bar */}
|
|
<div className="flex-1 min-w-0">
|
|
<div className="flex items-center justify-between mb-1">
|
|
<span className={`font-semibold ${isActive ? 'text-blue-700' : 'text-gray-900'}`}>
|
|
{row.config.label}
|
|
</span>
|
|
<div className="flex items-center gap-2">
|
|
<span className="text-sm font-bold text-gray-700">
|
|
{row.count.toLocaleString()}
|
|
</span>
|
|
<span className="text-xs text-gray-500">
|
|
({row.percentage.toFixed(0)}%)
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Progress Bar */}
|
|
<div className="h-3 bg-gray-100 rounded-full overflow-hidden">
|
|
<div
|
|
className="h-full rounded-full transition-all duration-500"
|
|
style={{
|
|
width: `${row.barWidth}%`,
|
|
backgroundColor: isActive ? '#3b82f6' : row.config.color,
|
|
}}
|
|
/>
|
|
</div>
|
|
|
|
{/* Sentiment mini-stats */}
|
|
<div className="flex items-center gap-3 mt-1.5 text-xs">
|
|
<span className="text-green-600 font-medium">
|
|
👍 {row.positiveCount}
|
|
</span>
|
|
<span className="text-red-600 font-medium">
|
|
👎 {row.negativeCount}
|
|
</span>
|
|
<span className="text-gray-500">
|
|
{row.reviewCount} reviews
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Health Indicator */}
|
|
<div className="flex-shrink-0">
|
|
{row.health === 'good' && (
|
|
<TrendingUp className="w-5 h-5 text-green-500" />
|
|
)}
|
|
{row.health === 'critical' && (
|
|
<TrendingDown className="w-5 h-5 text-red-500" />
|
|
)}
|
|
{row.health === 'warning' && (
|
|
<Minus className="w-5 h-5 text-gray-400" />
|
|
)}
|
|
</div>
|
|
</div>
|
|
</button>
|
|
);
|
|
})}
|
|
</div>
|
|
)}
|
|
|
|
{/* Legend */}
|
|
<div className="flex items-center justify-center gap-4 mt-4 pt-3 border-t border-gray-100 text-xs text-gray-500">
|
|
<span className="flex items-center gap-1">
|
|
<TrendingUp className="w-3 h-3 text-green-500" /> Mostly positive
|
|
</span>
|
|
<span className="flex items-center gap-1">
|
|
<Minus className="w-3 h-3 text-gray-400" /> Mixed
|
|
</span>
|
|
<span className="flex items-center gap-1">
|
|
<TrendingDown className="w-3 h-3 text-red-500" /> Needs attention
|
|
</span>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|