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')}
)}
);
}