feat(frontend): Add BusinessReport component for 6-section €60 report
- Create BusinessReport.tsx with 6 sections: 1. Executive Summary (health score, rating, momentum) 2. Risk Scorecard (indicators with colors/trends) 3. Critical Issues (evidence, solutions, timelines) 4. Strengths to Protect (quotes, leverage actions) 5. Action Matrix (effort/impact quadrants) 6. 90-Day Tracking (KPI targets table) - Update types.ts with new interfaces: - SynthesisV2 for new report format - LegacySynthesis for backwards compatibility - Type guard isSynthesisV2() for runtime detection - Update ReportTab to auto-detect synthesis version - Update AnalystReport, ReviewIQDashboard, StoryView for backwards compatibility with union type Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,19 +1,30 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { RefreshCw, BarChart3 } from 'lucide-react';
|
||||
import { RefreshCw } 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 { FilterBar } from './FilterBar';
|
||||
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';
|
||||
import { ExplorerView } from './ExplorerView';
|
||||
import type { URTDomain, Synthesis, LegacySynthesis } from './types';
|
||||
import { isSynthesisV2 } from './types';
|
||||
|
||||
// Helper to extract legacy fields from either synthesis format
|
||||
function getLegacyInsight(synthesis: Synthesis | null | undefined, field: keyof LegacySynthesis): string | undefined {
|
||||
if (!synthesis) return undefined;
|
||||
if (isSynthesisV2(synthesis)) {
|
||||
// V2 doesn't have these fields, return undefined
|
||||
return undefined;
|
||||
}
|
||||
return (synthesis as LegacySynthesis)[field] as string | undefined;
|
||||
}
|
||||
|
||||
interface ReviewIQDashboardProps {
|
||||
jobId?: string | null;
|
||||
@@ -22,13 +33,7 @@ interface ReviewIQDashboardProps {
|
||||
|
||||
/**
|
||||
* 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
|
||||
* Shows data exploration view with charts, tables, and trend explorer.
|
||||
*/
|
||||
function ReviewIQDashboardInner({ jobId, businessId }: ReviewIQDashboardProps) {
|
||||
const { filters, setURTDomain } = useReviewIQFilters();
|
||||
@@ -45,9 +50,6 @@ function ReviewIQDashboardInner({ jobId, businessId }: ReviewIQDashboardProps) {
|
||||
spansPageSize: 10,
|
||||
});
|
||||
|
||||
const handleIssuesPageChange = (page: number) => setIssuesPage(page);
|
||||
const handleSpansPageChange = (page: number) => setSpansPage(page);
|
||||
|
||||
// No job selected
|
||||
if (!jobId && !businessId) {
|
||||
return <DashboardEmpty />;
|
||||
@@ -68,106 +70,110 @@ function ReviewIQDashboardInner({ jobId, businessId }: ReviewIQDashboardProps) {
|
||||
return <DashboardEmpty />;
|
||||
}
|
||||
|
||||
// Handle domain click for filtering
|
||||
const handleDomainClick = (domain: URTDomain) => {
|
||||
setURTDomain(filters.urtDomain === domain ? null : domain);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* ═══════════════════════════════════════════════════════════════
|
||||
HEADER
|
||||
═══════════════════════════════════════════════════════════════ */}
|
||||
{/* 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>
|
||||
<h1 className="text-xl font-semibold text-gray-900">Data Explorer</h1>
|
||||
<p className="text-sm text-gray-500">
|
||||
{data.overview.total_reviews.toLocaleString()} reviews · {data.overview.total_spans.toLocaleString()} insights
|
||||
</p>
|
||||
</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"
|
||||
className="px-4 py-2 bg-slate-900 text-white rounded-lg font-medium hover:bg-slate-800 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 */}
|
||||
{/* Data View */}
|
||||
<DataView
|
||||
data={data}
|
||||
filters={filters}
|
||||
setURTDomain={setURTDomain}
|
||||
issuesPage={issuesPage}
|
||||
spansPage={spansPage}
|
||||
setIssuesPage={setIssuesPage}
|
||||
setSpansPage={setSpansPage}
|
||||
jobId={jobId || undefined}
|
||||
businessId={businessId || undefined}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Data view - the detailed charts and tables
|
||||
*/
|
||||
function DataView({
|
||||
data,
|
||||
filters,
|
||||
setURTDomain,
|
||||
issuesPage,
|
||||
spansPage,
|
||||
setIssuesPage,
|
||||
setSpansPage,
|
||||
jobId,
|
||||
businessId,
|
||||
}: {
|
||||
data: NonNullable<ReturnType<typeof useReviewIQAnalytics>['data']>;
|
||||
filters: ReturnType<typeof useReviewIQFilters>['filters'];
|
||||
setURTDomain: ReturnType<typeof useReviewIQFilters>['setURTDomain'];
|
||||
issuesPage: number;
|
||||
spansPage: number;
|
||||
setIssuesPage: (page: number) => void;
|
||||
setSpansPage: (page: number) => void;
|
||||
jobId?: string;
|
||||
businessId?: string;
|
||||
}) {
|
||||
const handleDomainClick = (domain: URTDomain) => {
|
||||
setURTDomain(filters.urtDomain === domain ? null : domain);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Filters */}
|
||||
<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}
|
||||
synthesis={data.synthesis}
|
||||
/>
|
||||
|
||||
{/* ═══════════════════════════════════════════════════════════════
|
||||
SECTION 2: EXPLORE (Sentiment + Categories)
|
||||
Side-by-side: How customers feel + What they talk about
|
||||
═══════════════════════════════════════════════════════════════ */}
|
||||
{/* Sentiment + Categories */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
<SentimentPie
|
||||
data={data.sentiment.distribution}
|
||||
insight={data.synthesis?.sentiment_insight}
|
||||
insight={getLegacyInsight(data.synthesis, 'sentiment_headline')}
|
||||
/>
|
||||
<IntensityHeatmap
|
||||
data={data.urt.domains}
|
||||
insight={data.synthesis?.category_insight}
|
||||
highlightDomain={data.synthesis?.priority_domain}
|
||||
insight={getLegacyInsight(data.synthesis, 'category_headline')}
|
||||
highlightDomain={getLegacyInsight(data.synthesis, 'primary_problem_code')?.charAt(0)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* ═══════════════════════════════════════════════════════════════
|
||||
SECTION 3: ACTION (Opportunity Matrix)
|
||||
What to fix - prioritized by impact vs effort
|
||||
═══════════════════════════════════════════════════════════════ */}
|
||||
{/* Opportunity Matrix */}
|
||||
<OpportunityMatrix matrix={data.insights.opportunity_matrix} />
|
||||
|
||||
{/* ═══════════════════════════════════════════════════════════════
|
||||
SECTION 4: TRENDS (Timeline)
|
||||
How things change over time
|
||||
═══════════════════════════════════════════════════════════════ */}
|
||||
{/* Timeline */}
|
||||
<TimelineChart
|
||||
data={data.timeline}
|
||||
insight={data.synthesis?.timeline_insight}
|
||||
annotations={data.synthesis?.timeline_annotations}
|
||||
insight={getLegacyInsight(data.synthesis, 'timeline_headline')}
|
||||
granularity={(data.filters_applied?.granularity as 'day' | 'week' | 'month' | 'year') || 'week'}
|
||||
/>
|
||||
|
||||
{/* ═══════════════════════════════════════════════════════════════
|
||||
SECTION 5: DEEP DIVE (Tables)
|
||||
Detailed issues and individual mentions
|
||||
═══════════════════════════════════════════════════════════════ */}
|
||||
{/* Tables */}
|
||||
<div className="grid lg:grid-cols-2 gap-6">
|
||||
<IssuesTable issues={data.issues} onPageChange={handleIssuesPageChange} />
|
||||
<SpansTable spans={data.spans} onPageChange={handleSpansPageChange} />
|
||||
<IssuesTable issues={data.issues} onPageChange={setIssuesPage} />
|
||||
<SpansTable spans={data.spans} onPageChange={setSpansPage} />
|
||||
</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>
|
||||
{/* Trend Explorer */}
|
||||
<div className="mt-8">
|
||||
<h2 className="text-lg font-semibold text-gray-900 mb-4">Trend Explorer</h2>
|
||||
<ExplorerView jobId={jobId} businessId={businessId} />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user