"use client"; import { useEffect, useMemo } from "react"; import type { QueryKey } from "@tanstack/react-query"; import type { Issue, IssueAssigneeGroup, IssueStatus, Project, } from "@multica/core/types"; import { useWorkspaceId } from "@multica/core/hooks"; import { dateOnlyToLocalDate } from "@multica/core/issues/date"; import type { AssigneeGroupedIssuesFilter, IssueSortParam, MyIssuesFilter, } from "@multica/core/issues/queries"; import { buildIssueSurfaceQueryPlan, type IssueSurfaceQueryPlan, } from "@multica/core/issues/surface/query-plan"; import type { IssueScope } from "@multica/core/issues/surface/scope"; import type { IssueDateFilter } from "@multica/core/issues/stores/view-store"; import { useViewStore } from "@multica/core/issues/stores/view-store-context"; import type { IssueFilters } from "../utils/filter"; import type { ChildProgress } from "../components/list-row"; import type { IssueSurfaceMode } from "./types"; import type { IssueSurfaceActivity } from "./activity"; import type { IssueSurfaceActions } from "./actions-context"; import { type IssueSurfaceSelection, useCreateIssueSurfaceSelection, } from "./selection-context"; import type { IssueCreateDefaults } from "./types"; import { useIssueSurfaceActions, type MoveIssueUpdates, } from "./use-issue-surface-actions"; import { useIssueSurfaceData } from "./use-issue-surface-data"; interface UseIssueSurfaceControllerInput { scope: IssueScope; modes: IssueSurfaceMode[]; createDefaults?: IssueCreateDefaults; } export interface IssueSurfaceController { scopeKey: string; projectId?: string; createDefaults: IssueCreateDefaults; viewMode: IssueSurfaceMode; allowGantt: boolean; surfaceIssues: Issue[]; projectIssues: Issue[]; issues: Issue[]; swimlaneIssues: Issue[]; filteredGanttIssues: Issue[]; assigneeGroups?: IssueAssigneeGroup[]; assigneeGroupQueryKey?: QueryKey; assigneeGroupFilter?: AssigneeGroupedIssuesFilter; filter: MyIssuesFilter; loadMoreScope?: string; loadMoreFilter?: MyIssuesFilter; sort: IssueSortParam; ganttIssues: Issue[]; visibleStatuses: IssueStatus[]; hiddenStatuses: IssueStatus[]; activeFilters: Omit; activity: IssueSurfaceActivity; actions: IssueSurfaceActions; selection: IssueSurfaceSelection; childProgressMap: Map; projectMap: Map; isLoading: boolean; /** See IssueSurfaceData.isRefreshing — placeholder-backed revalidation. */ isRefreshing: boolean; isEmpty: boolean; openCreateIssue: (defaults?: IssueCreateDefaults) => void; moveIssue: ( issueId: string, updates: MoveIssueUpdates, onSettled?: () => void, ) => void; } function issueDateFilterToApiParams(filter: IssueDateFilter | null) { if (!filter) return {}; const from = dateOnlyToLocalDate(filter.from); const to = dateOnlyToLocalDate(filter.to); if (!from || !to) return {}; const start = from <= to ? from : to; const endSource = from <= to ? to : from; const end = new Date(endSource); end.setDate(end.getDate() + 1); return { date_field: filter.field, date_start: start.toISOString(), date_end: end.toISOString(), }; } export function useIssueSurfaceController({ scope, modes, createDefaults, }: UseIssueSurfaceControllerInput): IssueSurfaceController { const wsId = useWorkspaceId(); const queryPlan = useMemo( () => buildIssueSurfaceQueryPlan(scope), [scope], ); const scopeKey = queryPlan.scopeKey; const projectId = scope.type === "project" ? scope.projectId : undefined; const viewMode = useViewStore((s) => s.viewMode); const setViewMode = useViewStore((s) => s.setViewMode); const grouping = useViewStore((s) => s.grouping); const sortBy = useViewStore((s) => s.sortBy); const sortDirection = useViewStore((s) => s.sortDirection); const dateFilter = useViewStore((s) => s.dateFilter); const statusFilters = useViewStore((s) => s.statusFilters); const priorityFilters = useViewStore((s) => s.priorityFilters); const assigneeFilters = useViewStore((s) => s.assigneeFilters); const includeNoAssignee = useViewStore((s) => s.includeNoAssignee); const creatorFilters = useViewStore((s) => s.creatorFilters); const projectFilters = useViewStore((s) => s.projectFilters); const includeNoProject = useViewStore((s) => s.includeNoProject); const labelFilters = useViewStore((s) => s.labelFilters); const agentRunningFilter = useViewStore((s) => s.agentRunningFilter); const showSubIssues = useViewStore((s) => s.showSubIssues); const cardProperties = useViewStore((s) => s.cardProperties); const swimlaneGrouping = useViewStore((s) => s.swimlaneGrouping); const allowedModes = useMemo(() => new Set(modes), [modes]); const fallbackMode = modes[0] ?? "list"; const effectiveViewMode = allowedModes.has(viewMode as IssueSurfaceMode) ? (viewMode as IssueSurfaceMode) : fallbackMode; useEffect(() => { if (!allowedModes.has(viewMode as IssueSurfaceMode)) { setViewMode(fallbackMode); } }, [allowedModes, fallbackMode, setViewMode, viewMode]); const resolvedCreateDefaults = useMemo( () => ({ ...queryPlan.createDefaults, ...createDefaults }), [createDefaults, queryPlan.createDefaults], ); const dateParams = useMemo( () => issueDateFilterToApiParams(dateFilter), [dateFilter], ); const sort = useMemo( () => ({ sort_by: sortBy, sort_direction: sortBy !== "position" ? sortDirection : undefined, ...dateParams, }), [dateParams, sortBy, sortDirection], ); const selection = useCreateIssueSurfaceSelection( scopeKey, `${scopeKey}:${effectiveViewMode}`, ); const usesAssigneeBoard = effectiveViewMode === "board" && grouping === "assignee"; const usesGantt = effectiveViewMode === "gantt" && !!projectId; const projectFilterState = useMemo( () => ({ projectFilters: scope.type === "project" ? [] : projectFilters, includeNoProject: scope.type === "project" ? false : includeNoProject, }), [includeNoProject, projectFilters, scope.type], ); const { projectFilters: viewProjectFilters, includeNoProject: viewIncludeNoProject } = projectFilterState; const data = useIssueSurfaceData({ wsId, queryPlan, projectId, usesAssigneeBoard, usesGantt, sort, statusFilters, priorityFilters, assigneeFilters, includeNoAssignee, creatorFilters, projectFilters: viewProjectFilters, includeNoProject: viewIncludeNoProject, labelFilters, agentRunningFilter, showSubIssues, loadProjects: cardProperties.project || (effectiveViewMode === "swimlane" && swimlaneGrouping === "project"), }); const { actions, openCreateIssue, moveIssue } = useIssueSurfaceActions({ createDefaults: resolvedCreateDefaults, }); return { scopeKey, projectId, createDefaults: resolvedCreateDefaults, viewMode: effectiveViewMode, allowGantt: allowedModes.has("gantt") && !!projectId, ...data, sort, actions, selection, openCreateIssue, moveIssue, }; }