All files / src/hooks/useTapToAssign useTapToAssign.ts

96.15% Statements 25/26
90.47% Branches 19/21
87.5% Functions 7/8
100% Lines 25/25

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                                                                                                  84x   84x 5x 5x           84x 3x 3x       84x   84x   7x 1x     6x 1x 1x   5x   7x 3x 2x 1x   1x   5x 5x   5x           84x       18x              
/**
 * useTapToAssign (Story 15.3)
 *
 * Touch-friendly slot assignment: tap a session to "arm" it, then tap a target slot to
 * place it. Resolves the right backend mode from the armed source and the target state:
 *
 * - armed from the pool + empty target  → ASSIGN
 * - armed from the pool + occupied target → INSERT (push the occupant + reflow)
 * - armed from a slot  + empty target   → ASSIGN (move)
 * - armed from a slot  + occupied target → SWAP
 *
 * Extracted from the inline tap logic in DragDropSlotAssignment (14.G.3) and generalised to
 * also support tap-to-swap. Desktop keeps native HTML5 drag; this hook drives touch viewports.
 */
 
import { useCallback, useState } from 'react';
import type { SlotAssignmentMode } from '@/services/slotAssignmentService/slotAssignmentService';
 
/** The currently "picked up" session. `fromSlotKey` is null when armed from the pool. */
export interface ArmedSession {
  sessionSlug: string;
  fromSlotKey: string | null;
}
 
export interface UseTapToAssignArgs {
  /** Commit an assignment. Receives the resolved mode. */
  commit: (
    sessionSlug: string,
    targetSlotKey: string,
    mode: SlotAssignmentMode
  ) => Promise<void> | void;
}
 
export interface UseTapToAssignReturn {
  armed: ArmedSession | null;
  armedSessionSlug: string | null;
  /** True while any session is armed (used to highlight candidate target slots). */
  hasArmed: boolean;
  isArmed: (sessionSlug: string) => boolean;
  /** Tap a pool session: arms it, or disarms if it was already the armed pool session. */
  armFromPool: (sessionSlug: string) => void;
  /** Tap an assigned slot: arms its occupant for a move/swap, or disarms if re-tapped. */
  armFromSlot: (sessionSlug: string, slotKey: string) => void;
  /** Tap a target slot. No-op if nothing is armed. */
  tapSlot: (targetSlotKey: string, occupiedSessionSlug: string | null) => Promise<void>;
  clear: () => void;
}
 
export function useTapToAssign({ commit }: UseTapToAssignArgs): UseTapToAssignReturn {
  const [armed, setArmed] = useState<ArmedSession | null>(null);
 
  const armFromPool = useCallback((sessionSlug: string) => {
    setArmed((prev) =>
      prev && prev.fromSlotKey === null && prev.sessionSlug === sessionSlug
        ? null
        : { sessionSlug, fromSlotKey: null }
    );
  }, []);
 
  const armFromSlot = useCallback((sessionSlug: string, slotKey: string) => {
    setArmed((prev) =>
      prev && prev.fromSlotKey === slotKey ? null : { sessionSlug, fromSlotKey: slotKey }
    );
  }, []);
 
  const clear = useCallback(() => setArmed(null), []);
 
  const tapSlot = useCallback(
    async (targetSlotKey: string, occupiedSessionSlug: string | null) => {
      if (!armed) {
        return;
      }
      // Tapping the slot you armed from cancels the pick-up.
      if (armed.fromSlotKey === targetSlotKey) {
        setArmed(null);
        return;
      }
      const targetOccupied = !!occupiedSessionSlug && occupiedSessionSlug !== armed.sessionSlug;
      let mode: SlotAssignmentMode;
      if (!targetOccupied) {
        mode = 'ASSIGN';
      } else if (armed.fromSlotKey) {
        mode = 'SWAP';
      } else {
        mode = 'INSERT';
      }
      try {
        await commit(armed.sessionSlug, targetSlotKey, mode);
      } finally {
        setArmed(null);
      }
    },
    [armed, commit]
  );
 
  return {
    armed,
    armedSessionSlug: armed?.sessionSlug ?? null,
    hasArmed: armed !== null,
    isArmed: (sessionSlug: string) => armed?.sessionSlug === sessionSlug,
    armFromPool,
    armFromSlot,
    tapSlot,
    clear,
  };
}