'use client'; /** * DeliveryLists — the delivery section body (two tables: recently * completed + active). Reads `search` and `task_type` URL params * written by the shared `` chrome and refetches on * change. * * Endpoint configuration: * - `completedApiEndpoint` / `inProgressApiEndpoint` are the two * per-bucket GET endpoints. Defaults match the hub's pre-migration * routes (`/api/delivery/completed`, `/api/delivery/in-progress`). * * Coupling constraint — `searchParamKey` / `taskTypeParamKey`: * These props serve TWO purposes: * 1. URL READS — keys this component reads via `useSearchParams()`. * MUST match the consuming chrome's `section.search.paramKey` / * `section.filter.paramKey` (the chrome WRITES the URL params). * 2. API WRITES — keys this component sends as query params on the * outbound fetch to `{completedApiEndpoint,inProgressApiEndpoint}`. * The hub API contract uses `'search'` / `'task_type'`; embedders * reverse-proxying those routes must preserve the same names OR * rewrite the inbound query string on the proxy side. * * Defaults align with `OPENFRAME_DEV_SECTIONS.delivery.{search.paramKey,filter.paramKey}` * AND the hub API contract, so the OpenFrame zero-config case "just * works". Custom chrome overriding the param keys must override BOTH * ends consistently AND ensure the backend reads the same names. */ import { useEffect, useState } from 'react'; import { useSearchParams, useRouter, usePathname } from '../../../embed-shims'; import type { DeliveryResponse } from '../../../types/delivery'; import { DeliveryTable } from './delivery-table'; import { EmptyState } from '../../empty-state'; import { LoadError } from '../../ui/error-state'; import { DEV_SECTION_PARAM_KEYS } from '../../../utils/dev-sections/dev-section-param-keys'; import { STICKY_HEADER_OFFSET_PX } from '../../../utils/same-page-hash-nav'; import { contentFetch } from '../../../utils/embed-content-fetch'; import { useScrollToHash } from '../../../hooks/use-scroll-to-hash'; const DEFAULT_COMPLETED_ENDPOINT = '/api/delivery/completed'; const DEFAULT_IN_PROGRESS_ENDPOINT = '/api/delivery/in-progress'; // Param keys sourced from the shared registry (see RoadmapView) — single source for the // chrome's written `?key=` and this view's read. const DEFAULT_SEARCH_PARAM_KEY = DEV_SECTION_PARAM_KEYS.search; const DEFAULT_TASK_TYPE_PARAM_KEY = DEV_SECTION_PARAM_KEYS.deliveryTaskType; export interface DeliveryListsProps { /** GET endpoint for the "Recently Completed" bucket. Default * `/api/delivery/completed`. */ completedApiEndpoint?: string; /** GET endpoint for the "Active Tasks" bucket. Default * `/api/delivery/in-progress`. */ inProgressApiEndpoint?: string; /** URL param key for the search input. MUST match the consuming * chrome's `section.search.paramKey`. Default `'search'`. */ searchParamKey?: string; /** URL param key for the task-type filter. MUST match the consuming * chrome's `section.filter.paramKey`. Default `'task_type'`. */ taskTypeParamKey?: string; } export function DeliveryLists({ completedApiEndpoint = DEFAULT_COMPLETED_ENDPOINT, inProgressApiEndpoint = DEFAULT_IN_PROGRESS_ENDPOINT, searchParamKey = DEFAULT_SEARCH_PARAM_KEY, taskTypeParamKey = DEFAULT_TASK_TYPE_PARAM_KEY, }: DeliveryListsProps = {}) { const searchParams = useSearchParams(); const router = useRouter(); const pathname = usePathname(); const [data, setData] = useState(null); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(null); // Get filter state from URL const searchQuery = searchParams.get(searchParamKey) || ''; const taskTypeFilter = searchParams.get(taskTypeParamKey) || 'all'; useEffect(() => { async function fetchDeliveryData() { try { setIsLoading(true); setError(null); // Build query parameters for filtering. The outbound key names // mirror the inbound URL-param keys — see "Coupling constraint" // in the file docblock for why. const params = new URLSearchParams(); if (searchQuery) { params.set(searchParamKey, searchQuery); } if (taskTypeFilter && taskTypeFilter !== 'all') { params.set(taskTypeParamKey, taskTypeFilter); } const queryString = params.toString(); const queryParam = queryString ? `?${queryString}` : ''; // Fetch completed and in-progress tasks separately with filters const [completedResponse, inProgressResponse] = await Promise.all([ contentFetch(`${completedApiEndpoint}${queryParam}`), contentFetch(`${inProgressApiEndpoint}${queryParam}`), ]); if (!completedResponse.ok || !inProgressResponse.ok) { throw new Error('Failed to fetch delivery items'); } const [completedResult, inProgressResult] = await Promise.all([ completedResponse.json(), inProgressResponse.json(), ]); setData({ completed: completedResult.items || [], inProgress: inProgressResult.items || [], }); } catch (err) { console.error('Error fetching delivery items:', err); setError('Failed to load delivery items. Please try again later.'); } finally { setIsLoading(false); } } fetchDeliveryData(); }, [searchQuery, taskTypeFilter, completedApiEndpoint, inProgressApiEndpoint, searchParamKey, taskTypeParamKey]); const filteredCompleted = data?.completed || []; const filteredInProgress = data?.inProgress || []; // Deep-link hash dispatch — `?search=#delivery-` from a chat // card or a linked-delivery card on a ticket. Shared hook owns the // poll-until-mount + hashchange-listener wiring (same instance used // by RoadmapView). 96 matches the sticky-header offset every // hash-scroll surface in the app uses. useScrollToHash(data, { headerOffset: STICKY_HEADER_OFFSET_PX }); const showCompleted = true; const showInProgress = true; const hasActiveFilters = searchQuery !== '' || taskTypeFilter !== 'all'; const hasResults = (showCompleted && filteredCompleted.length > 0) || (showInProgress && filteredInProgress.length > 0); // Error state — consume lib's canonical LoadError so ODS tokens + // retry affordance stay in lockstep with every other surface. if (error) { return (
window.location.reload()} />
); } return (
{/* Empty state if no results after filtering */} {!isLoading && !hasResults && ( hasActiveFilters ? ( { const params = new URLSearchParams(searchParams.toString()); params.delete(searchParamKey); params.delete(taskTypeParamKey); router.replace(`${pathname}?${params.toString()}`, { scroll: false }); }} /> ) : ( ) )} {/* Completed Tasks Table */} {showCompleted && (hasResults || isLoading) && (

Recently Completed:

)} {/* In Progress Tasks Table */} {showInProgress && (hasResults || isLoading) && (

Active Tasks:

)}
); }