import { useTranslation } from 'react-i18next'; import { LocalTask } from '../../api'; import { matchText } from '../../lib/utils'; import { COLUMN_LIST, SortMode, StatusColumn } from '../../lib/urlState'; import { filterTasksByScope, type TaskScope } from '../../lib/taskScope'; import { FilterBar } from './FilterBar'; import { LocalTaskListItem } from './TaskListItem'; import { RailPanel } from './RailPanel'; interface TaskListPanelProps { localTasks: LocalTask[]; selectedStatus: 'all' | StatusColumn; sortMode: SortMode; searchQuery: string; activeTaskId: number | null; onStatusChange: (status: 'all' | StatusColumn) => void; onSortChange: (sort: SortMode) => void; onSearchChange: (q: string) => void; onSelectTask: (id: number) => void; onOpenCreate: () => void; /** * Owner scope (mine/all). Only meaningful when scopeEnabled — auth must be * on and the viewer known; otherwise everything is owner 'local' and the * control is hidden. */ scope?: TaskScope; onScopeChange?: (scope: TaskScope) => void; currentUserId?: string | null; scopeEnabled?: boolean; /** 'rail' 時は RailPanel を render する。default 'list'。 */ mode?: 'list' | 'rail'; /** rail mode 時の「リストに戻る」ボタンで呼ばれる。 */ onExitFocused?: () => void; } function ScopeToggle({ scope, mineCount, allCount, onScopeChange, }: { scope: TaskScope; mineCount: number; allCount: number; onScopeChange: (scope: TaskScope) => void; }) { const { t } = useTranslation('list'); const seg = (value: TaskScope, label: string, count: number) => ( ); return (
{seg('mine', t('scope.mine'), mineCount)} {seg('all', t('scope.all'), allCount)}
); } export function TaskListPanel({ localTasks: allTasks, selectedStatus, sortMode, searchQuery, activeTaskId, onStatusChange, onSortChange, onSearchChange, onSelectTask, onOpenCreate, scope = 'mine', onScopeChange, currentUserId = null, scopeEnabled = false, mode = 'list', onExitFocused, }: TaskListPanelProps) { const { t } = useTranslation('list'); // Owner scope is the outermost filter: status counts / search / sort all // operate on the scoped list so "自分" mode never counts others' tasks. const effectiveScope: TaskScope = scopeEnabled ? scope : 'all'; const localTasks = filterTasksByScope(allTasks, effectiveScope, currentUserId); if (mode === 'rail') { const localColumnsRail: Record = COLUMN_LIST.reduce((acc, s) => { acc[s] = localTasks.filter(t => (t.latestJob?.status ?? 'queued') === s); return acc; }, {} as Record); const allRail = Object.values(localColumnsRail).flat(); const baseRail = selectedStatus === 'all' ? allRail : localColumnsRail[selectedStatus] ?? []; const filteredRail = baseRail.filter(t => matchText(t.title, searchQuery) || matchText(t.body, searchQuery) || matchText(t.pieceName, searchQuery) || matchText(t.ownerName ?? '', searchQuery), ); return ( {})} /> ); } const localColumns: Record = COLUMN_LIST.reduce((acc, s) => { acc[s] = localTasks.filter(t => (t.latestJob?.status ?? 'queued') === s); return acc; }, {} as Record); const allLocalTasks = Object.values(localColumns).flat(); const baseList: LocalTask[] = selectedStatus === 'all' ? allLocalTasks : localColumns[selectedStatus] ?? []; const filtered = baseList.filter(t => matchText(t.title, searchQuery) || matchText(t.body, searchQuery) || matchText(t.pieceName, searchQuery) || matchText(t.ownerName ?? '', searchQuery) ).sort((a, b) => { if (sortMode === 'title') { return a.title.localeCompare(b.title); } if (sortMode === 'status') { const aStatus = a.latestJob?.status ?? 'queued'; const bStatus = b.latestJob?.status ?? 'queued'; return aStatus.localeCompare(bStatus); } return new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime(); }); const counts: Record = {}; for (const s of COLUMN_LIST) { counts[s] = localColumns[s]?.length ?? 0; } const totalCount = allLocalTasks.length; const runningCount = counts.running ?? 0; const waitingCount = (counts.waiting_human ?? 0) + (counts.waiting_subtasks ?? 0); const failedCount = counts.failed ?? 0; return (
{scopeEnabled && onScopeChange && ( )}
{totalCount} {t('counts.items')} {runningCount} {t('counts.running')} {waitingCount} {t('counts.waiting')} {failedCount > 0 && ( {failedCount} {t('counts.failed')} )}
{filtered.map(task => ( onSelectTask(task.id)} /> ))} {filtered.length === 0 && (
{t('empty')}
)}
); }