All files / src/components/organizer/EventPage/topicOverlay useTopicSelection.ts

100% Statements 35/35
77.27% Branches 17/22
100% Functions 10/10
100% Lines 33/33

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123                                        3x 3x                                                                 22x 22x 22x   22x   22x   22x 1x   22x 2x 2x     22x 4x 4x 4x     22x 3x       3x 3x           22x 6x 6x 6x 3x 3x 1x   2x 2x       22x 1x 1x       22x                              
/**
 * useTopicSelection (Epic 14, Story 14.F.3)
 *
 * The select-a-topic-for-an-event decision logic, lifted out of TopicDetailsPanel
 * so the focused overlay's card grid can reuse it once (no per-card duplication).
 *
 * Routing mirrors TopicDetailsPanel exactly:
 *   - stalenessScore < 50 (too recent)  → 'override'  (justification required)
 *   - else high similarity (>0.7)        → 'similar'   (confirm, justification optional)
 *   - else                               → commit directly
 *
 * On a successful select it calls `onConfirmed(topicCode)` (the overlay then pins
 * the topic and switches to brainstorming). Recompose, not rewrite (NFR9) — no
 * new backend; reuses `useSelectTopicForEvent` + `useSimilarTopics`.
 */
 
import { useState } from 'react';
import { useSelectTopicForEvent, useSimilarTopics } from '@/hooks/useTopics';
import type { Topic } from '@/types/topic.types';
 
export const SIMILARITY_THRESHOLD = 0.7;
export const RECENT_STALENESS = 50;
 
export type TopicSelectionMode = 'idle' | 'similar' | 'override';
 
export interface UseTopicSelectionOptions {
  eventCode: string;
  onConfirmed?: (topicCode: string) => void;
}
 
export interface UseTopicSelectionResult {
  /** The topic currently going through a confirm dialog (null when none). */
  actingTopic: Topic | null;
  /** Which confirm dialog (if any) is open. */
  mode: TopicSelectionMode;
  justification: string;
  setJustification: (value: string) => void;
  isPending: boolean;
  /** High-similarity (>0.7) topics for the acting topic — drives the similar dialog list. */
  similarTopics: Topic[];
  /** Does this topic have any >0.7 similarity score? (drives the inline card warning) */
  hasHighSimilarity: (topic: Topic) => boolean;
  /** Entry point from a card's Select button — routes to direct commit / similar / override. */
  requestSelect: (topic: Topic) => void;
  /** Confirm the acting topic from inside a dialog. */
  commit: () => void;
  /** Dismiss any open dialog without selecting. */
  cancel: () => void;
}
 
export function useTopicSelection({
  eventCode,
  onConfirmed,
}: UseTopicSelectionOptions): UseTopicSelectionResult {
  const [actingTopic, setActingTopic] = useState<Topic | null>(null);
  const [mode, setMode] = useState<TopicSelectionMode>('idle');
  const [justification, setJustification] = useState('');
 
  const selectMutation = useSelectTopicForEvent();
  // Only fetches when a topic is acting (id empty → query disabled).
  const { data: similarTopicsRaw } = useSimilarTopics(actingTopic?.topicCode ?? '');
 
  const hasHighSimilarity = (topic: Topic): boolean =>
    (topic.similarityScores ?? []).some((s) => (s.score ?? 0) > SIMILARITY_THRESHOLD);
 
  const similarTopics: Topic[] = (similarTopicsRaw ?? []).filter((st) => {
    const sim = (actingTopic?.similarityScores ?? []).find((s) => s.topicCode === st.topicCode);
    return !!sim && (sim.score ?? 0) > SIMILARITY_THRESHOLD;
  });
 
  const reset = () => {
    setActingTopic(null);
    setMode('idle');
    setJustification('');
  };
 
  const doCommit = (topic: Topic, just?: string) => {
    selectMutation.mutate(
      { eventCode, request: { topicCode: topic.topicCode, justification: just || undefined } },
      {
        onSuccess: () => {
          onConfirmed?.(topic.topicCode);
          reset();
        },
      }
    );
  };
 
  const requestSelect = (topic: Topic) => {
    setActingTopic(topic);
    setJustification('');
    if (topic.stalenessScore < RECENT_STALENESS) {
      setMode('override');
    } else if (hasHighSimilarity(topic)) {
      setMode('similar');
    } else {
      setMode('idle');
      doCommit(topic);
    }
  };
 
  const commit = () => {
    Eif (actingTopic) {
      doCommit(actingTopic, justification);
    }
  };
 
  return {
    actingTopic,
    mode,
    justification,
    setJustification,
    isPending: selectMutation.isPending,
    similarTopics,
    hasHighSimilarity,
    requestSelect,
    commit,
    cancel: reset,
  };
}
 
export default useTopicSelection;