diff --git a/web/components/reviewiq/insights/OpportunityMatrix.tsx b/web/components/reviewiq/insights/OpportunityMatrix.tsx
new file mode 100644
index 0000000..ad068cb
--- /dev/null
+++ b/web/components/reviewiq/insights/OpportunityMatrix.tsx
@@ -0,0 +1,325 @@
+'use client';
+
+import { Target, Zap, Clock, LayoutGrid } from 'lucide-react';
+import type { OpportunityMatrix as OpportunityMatrixType, OpportunityItem } from '../types';
+
+interface OpportunityMatrixProps {
+ matrix: OpportunityMatrixType | null;
+ onSubcodeClick?: (subcode: string) => void;
+}
+
+interface QuadrantProps {
+ title: string;
+ icon: React.ReactNode;
+ items: OpportunityItem[];
+ bgColor: string;
+ borderColor: string;
+ textColor: string;
+ iconBg: string;
+ onItemClick?: (subcode: string) => void;
+}
+
+function Quadrant({
+ title,
+ icon,
+ items,
+ bgColor,
+ borderColor,
+ textColor,
+ iconBg,
+ onItemClick,
+}: QuadrantProps) {
+ return (
+
+ {/* Header */}
+
+
{icon}
+
{title}
+
+ {items.length}
+
+
+
+ {/* Positioned items with dots and offset labels */}
+ {items.length === 0 ? (
+
+ None
+
+ ) : (
+
+ )}
+ {/* Clickable labels */}
+ {items.length > 0 && items.map((item, index) => {
+ const safeX = 15 + item.x * 70;
+ const safeY = 30 + item.y * 55;
+ const labelOffsetX = safeX < 50 ? 8 : -8;
+ const labelOffsetY = (index % 3 - 1) * 6;
+ const anchorX = safeX < 50 ? 'left-0' : 'right-0';
+
+ return (
+
+ );
+ })}
+
+ );
+}
+
+/**
+ * 2x2 Opportunity Matrix visualization.
+ * Shows issues categorized by frequency vs complexity with coordinate positioning.
+ */
+export function OpportunityMatrix({ matrix, onSubcodeClick }: OpportunityMatrixProps) {
+ if (!matrix) {
+ return null;
+ }
+
+ const totalItems =
+ matrix.quick_wins.length +
+ matrix.critical.length +
+ matrix.nice_to_have.length +
+ matrix.strategic.length;
+
+ if (totalItems === 0) {
+ return null;
+ }
+
+ return (
+
+
+
+
Opportunity Matrix
+
+ {totalItems} opportunities
+
+
+
+ {/* Matrix with L-shaped label area */}
+
+ {/* Y-axis label column */}
+
+
+ Effort →
+
+
+
+ {/* Main matrix area */}
+
+ {/* Matrix with arrows */}
+
+ {/* Coordinate axes - L-shaped from bottom-left origin */}
+
+
+
+ {/* Unified coordinate grid overlay */}
+
+
+
+ {/* Top Row: Simple solutions */}
+ }
+ items={matrix.nice_to_have}
+ bgColor="bg-gray-50"
+ borderColor="border-gray-200"
+ textColor="text-gray-700"
+ iconBg="bg-gray-200"
+ onItemClick={onSubcodeClick}
+ />
+ }
+ items={matrix.quick_wins}
+ bgColor="bg-green-50"
+ borderColor="border-green-200"
+ textColor="text-green-700"
+ iconBg="bg-green-200"
+ onItemClick={onSubcodeClick}
+ />
+
+ {/* Bottom Row: Complex solutions */}
+ }
+ items={matrix.strategic}
+ bgColor="bg-purple-50"
+ borderColor="border-purple-200"
+ textColor="text-purple-700"
+ iconBg="bg-purple-200"
+ onItemClick={onSubcodeClick}
+ />
+ }
+ items={matrix.critical}
+ bgColor="bg-red-50"
+ borderColor="border-red-200"
+ textColor="text-red-700"
+ iconBg="bg-red-200"
+ onItemClick={onSubcodeClick}
+ />
+
+
+
+
+ {/* X-axis label row */}
+
+
+ Frequency →
+
+
+
+
+
+ {/* Legend */}
+
+
+
+
Quick Wins: Fix first
+
+
+
+
Critical: High impact
+
+
+
+
Strategic: Plan ahead
+
+
+
+
Nice to Have: Low priority
+
+
+
+ );
+}
diff --git a/web/components/reviewiq/types.ts b/web/components/reviewiq/types.ts
new file mode 100644
index 0000000..e9c5913
--- /dev/null
+++ b/web/components/reviewiq/types.ts
@@ -0,0 +1,366 @@
+/**
+ * TypeScript types for ReviewIQ Dashboard.
+ */
+
+// ==================== API Response Types ====================
+
+export interface OverviewStats {
+ total_reviews: number;
+ total_spans: number;
+ open_issues: number;
+ avg_rating: number | null;
+ positive_count: number;
+ negative_count: number;
+ neutral_count: number;
+ mixed_count: number;
+}
+
+export interface SentimentDataPoint {
+ valence: string;
+ count: number; // Span count (mentions)
+ review_count: number; // Distinct reviews
+ percentage: number; // Based on review_count
+}
+
+export interface SentimentTrendPoint {
+ period: string;
+ positive: number;
+ negative: number;
+ neutral: number;
+ mixed: number;
+}
+
+export interface SentimentData {
+ distribution: SentimentDataPoint[];
+ trend: SentimentTrendPoint[];
+}
+
+export interface URTDomainPoint {
+ domain: string;
+ domain_name: string;
+ count: number; // Span count (mentions)
+ review_count: number; // Distinct reviews affected
+ percentage: number; // Based on review_count
+ positive_count: number; // Positive spans
+ negative_count: number; // Negative spans
+ neutral_count: number; // Neutral spans
+ positive_reviews: number; // Reviews with positive sentiment
+ negative_reviews: number; // Reviews with negative sentiment
+}
+
+export interface IntensityPoint {
+ domain: string;
+ intensity: string;
+ count: number;
+}
+
+export interface URTData {
+ domains: URTDomainPoint[];
+ intensity_heatmap: IntensityPoint[];
+}
+
+// ==================== Domain Scores ====================
+
+export interface DomainScore {
+ domain: string;
+ name: string;
+ score: number;
+ status: 'good' | 'warning' | 'critical';
+ trend: string | null;
+ positive_count: number;
+ negative_count: number;
+ total_count: number;
+}
+
+// ==================== Insights ====================
+
+export interface StrengthItem {
+ rank: number;
+ subcode: string;
+ subcode_name: string;
+ domain: string;
+ domain_name: string;
+ positive_percentage: number;
+ span_count: number;
+ marketing_angle: string | null;
+}
+
+export interface WeaknessItem {
+ rank: number;
+ issue_id: string | null;
+ subcode: string;
+ subcode_name: string;
+ domain: string;
+ domain_name: string;
+ negative_percentage: number;
+ span_count: number;
+ intensity: string | null;
+ solution: string | null;
+ solution_complexity: string | null;
+ projected_rating_impact: number | null;
+ owner: string | null;
+}
+
+export interface RatingSimulator {
+ current_rating: number;
+ if_fix_top_1: number | null;
+ if_fix_top_3: number | null;
+ potential_gain: number;
+}
+
+export interface OpportunityItem {
+ subcode: string;
+ x: number; // 0-1, frequency position within quadrant
+ y: number; // 0-1, effort position within quadrant
+}
+
+export interface OpportunityMatrix {
+ quick_wins: OpportunityItem[];
+ critical: OpportunityItem[];
+ nice_to_have: OpportunityItem[];
+ strategic: OpportunityItem[];
+}
+
+export interface Insights {
+ strengths: StrengthItem[];
+ weaknesses: WeaknessItem[];
+ rating_simulator: RatingSimulator | null;
+ opportunity_matrix: OpportunityMatrix | null;
+ executive_summary: string;
+}
+
+// ==================== Issues (Enriched) ====================
+
+export interface IssueItem {
+ issue_id: string;
+ primary_subcode: string;
+ subcode_name: string | null;
+ subcode_definition: string | null;
+ solution: string | null;
+ solution_complexity: string | null;
+ domain: string;
+ domain_name: string | null;
+ category_name: string | null;
+ default_owner: string | null;
+ negative_example: string | null;
+ entity: string | null;
+ state: string;
+ priority_score: number;
+ span_count: number;
+ max_intensity: string | null;
+ created_at: string | null;
+}
+
+export interface PaginatedIssues {
+ items: IssueItem[];
+ total: number;
+ page: number;
+ page_size: number;
+}
+
+// ==================== Spans ====================
+
+export interface SpanItem {
+ span_id: string;
+ span_text: string;
+ urt_primary: string | null;
+ valence: string | null;
+ intensity: string | null;
+ review_time: string | null;
+ source_review_id: string | null;
+ entity: string | null;
+}
+
+export interface PaginatedSpans {
+ items: SpanItem[];
+ total: number;
+ page: number;
+ page_size: number;
+}
+
+// ==================== Full Review (Drill-Down) ====================
+
+export interface ReviewSpan {
+ span_id: string;
+ span_text: string;
+ start_offset: number | null;
+ end_offset: number | null;
+ urt_primary: string | null;
+ urt_secondary: string[] | null;
+ valence: string | null;
+ intensity: string | null;
+ entity: string | null;
+}
+
+export interface FullReview {
+ review_id: string;
+ source: string;
+ rating: number | null;
+ review_text: string | null;
+ text_normalized: string | null; // Text used for span offset calculation
+ review_time: string | null;
+ author_name: string | null;
+ author_url: string | null;
+ review_url: string | null;
+ business_name: string | null;
+ urt_primary: string | null;
+ urt_secondary: string[] | null;
+ spans: ReviewSpan[];
+}
+
+// ==================== Timeline ====================
+
+export interface TimelinePoint {
+ date: string;
+ review_count: number;
+ span_count: number;
+ avg_rating: number | null;
+ positive_count: number;
+ negative_count: number;
+}
+
+// ==================== Main Response ====================
+
+export interface ReviewIQAnalyticsResponse {
+ overview: OverviewStats;
+ sentiment: SentimentData;
+ urt: URTData;
+ domain_scores: DomainScore[];
+ overall_experience_index: number | null;
+ insights: Insights;
+ issues: PaginatedIssues;
+ spans: PaginatedSpans;
+ timeline: TimelinePoint[];
+ filters_applied: Record;
+}
+
+// ==================== Filter Types ====================
+
+export type TimeRange = '7d' | '14d' | '30d' | '90d' | '1y' | 'all';
+export type Sentiment = 'positive' | 'neutral' | 'negative';
+export type URTDomain = 'O' | 'P' | 'J' | 'E' | 'A' | 'V' | 'R';
+export type Intensity = 'I1' | 'I2' | 'I3';
+export type SentimentView = 'all' | 'positive' | 'negative' | 'mixed';
+
+export interface ReviewIQFilters {
+ timeRange: TimeRange;
+ sentiment: Sentiment[];
+ urtDomain: URTDomain | null;
+ intensity: Intensity[];
+ brushRange: { start: string; end: string } | null;
+}
+
+export interface ReviewIQFilterContextValue {
+ filters: ReviewIQFilters;
+ setFilters: React.Dispatch>;
+ toggleSentiment: (s: Sentiment) => void;
+ setURTDomain: (domain: URTDomain | null) => void;
+ toggleIntensity: (i: Intensity) => void;
+ setTimeRange: (range: TimeRange) => void;
+ setBrushRange: (range: { start: string; end: string } | null) => void;
+ clearFilters: () => void;
+ hasActiveFilters: boolean;
+}
+
+// ==================== Chart Colors ====================
+
+export const SENTIMENT_COLORS = {
+ positive: '#22c55e',
+ neutral: '#eab308',
+ negative: '#ef4444',
+ mixed: '#f97316',
+} as const;
+
+export const VALENCE_COLORS: Record = {
+ 'V+': '#22c55e',
+ 'V0': '#eab308',
+ 'V-': '#ef4444',
+ 'V±': '#f97316',
+};
+
+export const DOMAIN_COLORS: Record = {
+ O: '#f97316', // Offering - orange
+ P: '#3b82f6', // People - blue
+ J: '#8b5cf6', // Journey - purple
+ E: '#06b6d4', // Environment - cyan
+ A: '#10b981', // Access - green
+ V: '#ec4899', // Value - pink
+ R: '#f59e0b', // Relationship - amber
+};
+
+export const INTENSITY_COLORS: Record = {
+ I1: '#fef08a', // Light yellow
+ I2: '#fbbf24', // Yellow
+ I3: '#f97316', // Orange
+};
+
+export const STATUS_COLORS: Record = {
+ good: '#22c55e', // Green
+ warning: '#eab308', // Yellow
+ critical: '#ef4444', // Red
+};
+
+// ==================== Display Mappings ====================
+
+export const VALENCE_LABELS: Record = {
+ 'V+': 'Positive',
+ 'V0': 'Neutral',
+ 'V-': 'Negative',
+ 'V±': 'Mixed',
+};
+
+export const DOMAIN_LABELS: Record = {
+ O: 'Offering',
+ P: 'People',
+ J: 'Journey',
+ E: 'Environment',
+ A: 'Access',
+ V: 'Value',
+ R: 'Relationship',
+};
+
+export const DOMAIN_OWNERS: Record = {
+ O: 'Operations / Product',
+ P: 'HR / Training',
+ J: 'Operations / Process',
+ E: 'Facilities / IT',
+ A: 'Compliance / Design',
+ V: 'Finance / Pricing',
+ R: 'Leadership / CX',
+};
+
+export const INTENSITY_LABELS: Record = {
+ I1: 'Low',
+ I2: 'Medium',
+ I3: 'High',
+};
+
+export const COMPLEXITY_LABELS: Record = {
+ simple: 'Quick Fix',
+ medium: 'Moderate Effort',
+ complex: 'Strategic Initiative',
+};
+
+// ==================== Domain Thresholds (from C2-KPI-Mapping-Guide) ====================
+
+export const DOMAIN_THRESHOLDS: Record = {
+ O: { green: 80, yellow: 60 },
+ P: { green: 85, yellow: 70 },
+ J: { green: 75, yellow: 55 },
+ E: { green: 80, yellow: 65 },
+ A: { green: 85, yellow: 70 },
+ V: { green: 70, yellow: 50 },
+ R: { green: 80, yellow: 60 },
+};
+
+// ==================== OEI Weights (from C2-KPI-Mapping-Guide) ====================
+
+export const DOMAIN_WEIGHTS: Record = {
+ O: 0.20,
+ P: 0.18,
+ J: 0.15,
+ E: 0.12,
+ A: 0.10,
+ V: 0.12,
+ R: 0.13,
+};