- 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>
172 lines
7.7 KiB
TypeScript
172 lines
7.7 KiB
TypeScript
'use client';
|
|
|
|
import { useState } from 'react';
|
|
import { RefreshCw, BarChart3 } from 'lucide-react';
|
|
import { ReviewIQFilterProvider, useReviewIQFilters } from '@/contexts/ReviewIQFilterContext';
|
|
import { useReviewIQAnalytics } from '@/hooks/useReviewIQAnalytics';
|
|
import { FilterBar } from './FilterBar';
|
|
import { DashboardSkeleton, DashboardError, DashboardEmpty } from './DashboardSkeleton';
|
|
import { SentimentPie } from './charts/SentimentPie';
|
|
import { IntensityHeatmap } from './charts/IntensityHeatmap';
|
|
import { TimelineChart } from './charts/TimelineChart';
|
|
import { IssuesTable } from './tables/IssuesTable';
|
|
import { SpansTable } from './tables/SpansTable';
|
|
import { ExecutiveSummary } from './insights/ExecutiveSummary';
|
|
import { OpportunityMatrix } from './insights/OpportunityMatrix';
|
|
import type { URTDomain } from './types';
|
|
|
|
interface ReviewIQDashboardProps {
|
|
jobId?: string | null;
|
|
businessId?: string | null;
|
|
}
|
|
|
|
/**
|
|
* Inner dashboard component that uses the filter context.
|
|
*
|
|
* Streamlined flow (no redundancy):
|
|
* 1. Hero: Executive Summary (rating, AI insights, #1 problem/strength, top complaints)
|
|
* 2. Explore: Sentiment + Category Heatmap (side by side)
|
|
* 3. Action: Opportunity Matrix (what to fix)
|
|
* 4. Trends: Timeline
|
|
* 5. Deep Dive: Issues & Spans tables
|
|
*/
|
|
function ReviewIQDashboardInner({ jobId, businessId }: ReviewIQDashboardProps) {
|
|
const { filters, setURTDomain } = useReviewIQFilters();
|
|
const [issuesPage, setIssuesPage] = useState(1);
|
|
const [spansPage, setSpansPage] = useState(1);
|
|
|
|
const { data, loading, error, refetch } = useReviewIQAnalytics({
|
|
jobId,
|
|
businessId,
|
|
filters,
|
|
issuesPage,
|
|
issuesPageSize: 10,
|
|
spansPage,
|
|
spansPageSize: 10,
|
|
});
|
|
|
|
const handleIssuesPageChange = (page: number) => setIssuesPage(page);
|
|
const handleSpansPageChange = (page: number) => setSpansPage(page);
|
|
|
|
// No job selected
|
|
if (!jobId && !businessId) {
|
|
return <DashboardEmpty />;
|
|
}
|
|
|
|
// Loading state
|
|
if (loading && !data) {
|
|
return <DashboardSkeleton />;
|
|
}
|
|
|
|
// Error state
|
|
if (error) {
|
|
return <DashboardError message={error} onRetry={refetch} />;
|
|
}
|
|
|
|
// No data
|
|
if (!data) {
|
|
return <DashboardEmpty />;
|
|
}
|
|
|
|
// Handle domain click for filtering
|
|
const handleDomainClick = (domain: URTDomain) => {
|
|
setURTDomain(filters.urtDomain === domain ? null : domain);
|
|
};
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
{/* ═══════════════════════════════════════════════════════════════
|
|
HEADER
|
|
═══════════════════════════════════════════════════════════════ */}
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-center gap-3">
|
|
<div className="p-2 bg-gradient-to-br from-blue-500 to-indigo-600 rounded-xl">
|
|
<BarChart3 className="w-6 h-6 text-white" />
|
|
</div>
|
|
<div>
|
|
<h1 className="text-2xl font-bold text-gray-900">ReviewIQ Analytics</h1>
|
|
<p className="text-sm text-gray-500">
|
|
{data.overview.total_reviews.toLocaleString()} reviews • {data.overview.total_spans.toLocaleString()} insights extracted
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<button
|
|
onClick={refetch}
|
|
disabled={loading}
|
|
className="px-4 py-2 bg-blue-600 text-white rounded-lg font-semibold hover:bg-blue-700 transition-colors flex items-center gap-2 disabled:opacity-50"
|
|
>
|
|
<RefreshCw className={`w-4 h-4 ${loading ? 'animate-spin' : ''}`} />
|
|
Refresh
|
|
</button>
|
|
</div>
|
|
|
|
{/* Active Filters Bar */}
|
|
<FilterBar />
|
|
|
|
{/* ═══════════════════════════════════════════════════════════════
|
|
SECTION 1: EXECUTIVE SUMMARY (Hero)
|
|
Rating, AI summary, #1 Problem, #1 Strength, Top Complaints
|
|
═══════════════════════════════════════════════════════════════ */}
|
|
<ExecutiveSummary
|
|
insights={data.insights}
|
|
avgRating={data.overview.avg_rating}
|
|
domainScores={data.domain_scores}
|
|
onDomainClick={handleDomainClick}
|
|
/>
|
|
|
|
{/* ═══════════════════════════════════════════════════════════════
|
|
SECTION 2: EXPLORE (Sentiment + Categories)
|
|
Side-by-side: How customers feel + What they talk about
|
|
═══════════════════════════════════════════════════════════════ */}
|
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
|
<SentimentPie data={data.sentiment.distribution} />
|
|
<IntensityHeatmap data={data.urt.domains} />
|
|
</div>
|
|
|
|
{/* ═══════════════════════════════════════════════════════════════
|
|
SECTION 3: ACTION (Opportunity Matrix)
|
|
What to fix - prioritized by impact vs effort
|
|
═══════════════════════════════════════════════════════════════ */}
|
|
<OpportunityMatrix matrix={data.insights.opportunity_matrix} />
|
|
|
|
{/* ═══════════════════════════════════════════════════════════════
|
|
SECTION 4: TRENDS (Timeline)
|
|
How things change over time
|
|
═══════════════════════════════════════════════════════════════ */}
|
|
<TimelineChart data={data.timeline} />
|
|
|
|
{/* ═══════════════════════════════════════════════════════════════
|
|
SECTION 5: DEEP DIVE (Tables)
|
|
Detailed issues and individual mentions
|
|
═══════════════════════════════════════════════════════════════ */}
|
|
<div className="grid lg:grid-cols-2 gap-6">
|
|
<IssuesTable issues={data.issues} onPageChange={handleIssuesPageChange} />
|
|
<SpansTable spans={data.spans} onPageChange={handleSpansPageChange} />
|
|
</div>
|
|
|
|
{/* Debug Info (dev only) */}
|
|
{process.env.NODE_ENV === 'development' && (
|
|
<details className="bg-gray-100 rounded-lg p-4 text-sm">
|
|
<summary className="cursor-pointer font-semibold text-gray-700">
|
|
Debug: Filters Applied
|
|
</summary>
|
|
<pre className="mt-2 bg-gray-900 text-gray-100 p-4 rounded-lg overflow-x-auto">
|
|
{JSON.stringify(data.filters_applied, null, 2)}
|
|
</pre>
|
|
</details>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Main ReviewIQ Dashboard with filter context provider.
|
|
*/
|
|
export function ReviewIQDashboard({ jobId, businessId }: ReviewIQDashboardProps) {
|
|
return (
|
|
<ReviewIQFilterProvider>
|
|
<ReviewIQDashboardInner jobId={jobId} businessId={businessId} />
|
|
</ReviewIQFilterProvider>
|
|
);
|
|
}
|