feat: implement multi-select status filter logic from reference hub
Support combined active chips for all/progress/hold/done and restore prior filters when toggling issue view. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,5 +1,10 @@
|
||||
import { useState } from 'react';
|
||||
|
||||
import { isDetailWindowOpen } from '../../lib/dualMonitor';
|
||||
|
||||
import {
|
||||
|
||||
FILTER_ALL,
|
||||
|
||||
isStatusChipActive,
|
||||
|
||||
@@ -13,8 +18,11 @@ interface Stats {
|
||||
|
||||
interface Stats {
|
||||
|
||||
activeStatus: string;
|
||||
onStatusChange: (status: string) => void;
|
||||
total: number;
|
||||
|
||||
inProgress: number;
|
||||
|
||||
review: number;
|
||||
|
||||
done: number;
|
||||
|
||||
@@ -27,11 +35,16 @@ const STAT_ACCENT = {
|
||||
interface DashboardHeaderProps {
|
||||
|
||||
quarter: string;
|
||||
|
||||
stats: Stats;
|
||||
|
||||
activeFilters: string[];
|
||||
|
||||
activeStatus,
|
||||
onStatusChange,
|
||||
issueFilterActive: boolean;
|
||||
|
||||
onToggleAll: () => void;
|
||||
|
||||
onToggleStatus: (key: CoreStatusFilter) => void;
|
||||
|
||||
onToggleIssue: () => void;
|
||||
|
||||
@@ -44,12 +57,48 @@ export function DashboardHeader({
|
||||
|
||||
|
||||
const STAT_ACCENT = {
|
||||
const statItems = [
|
||||
{ label: '전체', value: stats.total, statusKey: '전체' as const },
|
||||
{ label: '진행', value: stats.inProgress, statusKey: 'IN_PROGRESS' as const },
|
||||
{ label: '보류', value: stats.review, statusKey: 'REVIEW' as const },
|
||||
{ label: '완료', value: stats.done, statusKey: 'DONE' as const },
|
||||
{ label: '이슈', value: stats.issues, statusKey: 'ISSUES' as const },
|
||||
|
||||
전체: 'text-[#ffdb3a]',
|
||||
|
||||
IN_PROGRESS: 'text-[#10b981]',
|
||||
|
||||
REVIEW: 'text-[#ff9f0a]',
|
||||
|
||||
DONE: 'text-[#b0b0b0]',
|
||||
|
||||
ISSUES: 'text-[#ff5252]',
|
||||
|
||||
} as const;
|
||||
|
||||
|
||||
|
||||
type StatKey = keyof typeof STAT_ACCENT;
|
||||
|
||||
|
||||
|
||||
export function DashboardHeader({
|
||||
|
||||
quarter,
|
||||
|
||||
stats,
|
||||
|
||||
activeFilters,
|
||||
|
||||
issueFilterActive,
|
||||
|
||||
onToggleAll,
|
||||
|
||||
onToggleStatus,
|
||||
|
||||
onToggleIssue,
|
||||
|
||||
onOpenDetailWindow,
|
||||
|
||||
onOpenTaskManager,
|
||||
|
||||
}: DashboardHeaderProps) {
|
||||
|
||||
const [detailViewActive, setDetailViewActive] = useState(isDetailWindowOpen);
|
||||
|
||||
const quarterLabel = quarter.replace(/^(\d{4})-Q(\d)$/, '$1 $2분기 업무');
|
||||
|
||||
@@ -75,10 +124,9 @@ export function DashboardHeader({
|
||||
|
||||
statusKey: StatKey;
|
||||
|
||||
statusKey={item.statusKey}
|
||||
activeStatus={activeStatus}
|
||||
onClick: () => void;
|
||||
onClick={onStatusChange}
|
||||
|
||||
isActive: boolean;
|
||||
|
||||
}> = [
|
||||
|
||||
@@ -109,26 +157,21 @@ function StatDivider() {
|
||||
isActive: isStatusChipActive('IN_PROGRESS', activeFilters, issueFilterActive),
|
||||
|
||||
},
|
||||
statusKey: keyof typeof STAT_ACCENT;
|
||||
activeStatus: string;
|
||||
|
||||
onClick: (key: string) => void;
|
||||
{
|
||||
|
||||
label: '보류',
|
||||
|
||||
function StatClick({ label, value, statusKey, activeStatus, accent, onClick }: StatClickProps) {
|
||||
const isActive = activeStatus === statusKey;
|
||||
|
||||
const handleActivate = () => onClick(isActive ? '전체' : statusKey);
|
||||
|
||||
value: stats.review,
|
||||
|
||||
statusKey: 'REVIEW',
|
||||
|
||||
onClick: () => onToggleStatus('REVIEW'),
|
||||
onClick={handleActivate}
|
||||
|
||||
isActive: isStatusChipActive('REVIEW', activeFilters, issueFilterActive),
|
||||
|
||||
},
|
||||
handleActivate();
|
||||
|
||||
{
|
||||
|
||||
label: '완료',
|
||||
@@ -140,3 +183,4 @@ function StatClick({ label, value, statusKey, activeStatus, accent, onClick }: S
|
||||
onClick: () => onToggleStatus('DONE'),
|
||||
|
||||
isActive: isStatusChipActive('DONE', activeFilters, issueFilterActive),
|
||||
|
||||
68
frontend/src/lib/statusFilters.ts
Normal file
68
frontend/src/lib/statusFilters.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
import type { Task } from '../types';
|
||||
|
||||
export const FILTER_ALL = 'all' as const;
|
||||
export const CORE_STATUS_FILTERS = ['IN_PROGRESS', 'REVIEW', 'DONE'] as const;
|
||||
|
||||
export type CoreStatusFilter = (typeof CORE_STATUS_FILTERS)[number];
|
||||
|
||||
export const DEFAULT_STATUS_FILTERS: string[] = [
|
||||
FILTER_ALL,
|
||||
...CORE_STATUS_FILTERS,
|
||||
];
|
||||
|
||||
export function isCoreStatusFilter(key: string): key is CoreStatusFilter {
|
||||
return (CORE_STATUS_FILTERS as readonly string[]).includes(key);
|
||||
}
|
||||
|
||||
/** UI: 전체·진행·보류·완료 버튼 활성 표시 */
|
||||
export function isStatusChipActive(
|
||||
key: string,
|
||||
filters: string[],
|
||||
issueMode: boolean,
|
||||
): boolean {
|
||||
if (issueMode) return false;
|
||||
if (key === FILTER_ALL) return filters.includes(FILTER_ALL);
|
||||
if (isCoreStatusFilter(key)) {
|
||||
return filters.includes(FILTER_ALL) || filters.includes(key);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export function toggleAllFilter(current: string[]): string[] {
|
||||
if (current.includes(FILTER_ALL)) {
|
||||
return ['IN_PROGRESS'];
|
||||
}
|
||||
return [...DEFAULT_STATUS_FILTERS];
|
||||
}
|
||||
|
||||
export function toggleCoreFilter(current: string[], key: CoreStatusFilter): string[] {
|
||||
if (current.includes(FILTER_ALL)) {
|
||||
return CORE_STATUS_FILTERS.filter((k) => k !== key);
|
||||
}
|
||||
|
||||
const withoutAll = current.filter((f) => f !== FILTER_ALL);
|
||||
|
||||
if (withoutAll.includes(key)) {
|
||||
const next = withoutAll.filter((k) => k !== key);
|
||||
return next.length === 0 ? [...DEFAULT_STATUS_FILTERS] : next;
|
||||
}
|
||||
|
||||
const next = [...withoutAll, key];
|
||||
if (CORE_STATUS_FILTERS.every((k) => next.includes(k))) {
|
||||
return [FILTER_ALL, ...next];
|
||||
}
|
||||
return next;
|
||||
}
|
||||
|
||||
export function taskMatchesStatusFilters(task: Task, filters: string[]): boolean {
|
||||
if (filters.includes(FILTER_ALL)) return true;
|
||||
|
||||
return filters.some((key) => {
|
||||
if (key === 'IN_PROGRESS') return task.status === 'IN_PROGRESS';
|
||||
if (key === 'REVIEW') {
|
||||
return task.status === 'REVIEW' || task.status === 'CANCELLED' || task.status === 'TODO';
|
||||
}
|
||||
if (key === 'DONE') return task.status === 'DONE';
|
||||
return false;
|
||||
});
|
||||
}
|
||||
@@ -19,6 +19,13 @@ import { TaskManager } from '../components/dashboard/TaskManager';
|
||||
import { useSocket } from '../contexts/SocketContext';
|
||||
import { sendTaskSelected, openDetailWindow } from '../lib/dualMonitor';
|
||||
import { isRoutineTask, displayFlagsForTaskType } from '../lib/taskType';
|
||||
import {
|
||||
DEFAULT_STATUS_FILTERS,
|
||||
taskMatchesStatusFilters,
|
||||
toggleAllFilter,
|
||||
toggleCoreFilter,
|
||||
type CoreStatusFilter,
|
||||
} from '../lib/statusFilters';
|
||||
|
||||
const QUARTER = '2026-Q2';
|
||||
const SECTIONS = ['인사관리', '학습성장', '운영지원', '전산관리'] as const;
|
||||
@@ -31,7 +38,9 @@ const COLUMN_STYLES = [
|
||||
] as const;
|
||||
|
||||
export default function DashboardPage() {
|
||||
const [activeStatus, setActiveStatus] = useState('전체');
|
||||
const [activeFilters, setActiveFilters] = useState<string[]>([...DEFAULT_STATUS_FILTERS]);
|
||||
const [filtersBeforeIssue, setFiltersBeforeIssue] = useState<string[]>([...DEFAULT_STATUS_FILTERS]);
|
||||
const [issueFilterActive, setIssueFilterActive] = useState(false);
|
||||
const [showTaskManager, setShowTaskManager] = useState(false);
|
||||
const [activeTaskId, setActiveTaskId] = useState<string | null>(null);
|
||||
const [columnOrders, setColumnOrders] = useState<Record<string, string[]>>({});
|
||||
@@ -188,12 +197,30 @@ export default function DashboardPage() {
|
||||
};
|
||||
|
||||
const filtered = tasks.filter((t) => {
|
||||
if (activeStatus === '전체') return true;
|
||||
if (activeStatus === 'ISSUES') return !!t.issueNote;
|
||||
if (activeStatus === 'REVIEW') return t.status === 'REVIEW' || t.status === 'CANCELLED' || t.status === 'TODO';
|
||||
return t.status === activeStatus;
|
||||
if (issueFilterActive) return !!t.issueNote;
|
||||
return taskMatchesStatusFilters(t, activeFilters);
|
||||
});
|
||||
|
||||
const handleToggleAll = () => {
|
||||
setIssueFilterActive(false);
|
||||
setActiveFilters((prev) => toggleAllFilter(prev));
|
||||
};
|
||||
|
||||
const handleToggleStatus = (key: CoreStatusFilter) => {
|
||||
setIssueFilterActive(false);
|
||||
setActiveFilters((prev) => toggleCoreFilter(prev, key));
|
||||
};
|
||||
|
||||
const handleToggleIssue = () => {
|
||||
if (issueFilterActive) {
|
||||
setIssueFilterActive(false);
|
||||
setActiveFilters([...filtersBeforeIssue]);
|
||||
return;
|
||||
}
|
||||
setFiltersBeforeIssue([...activeFilters]);
|
||||
setIssueFilterActive(true);
|
||||
};
|
||||
|
||||
const sectionOptions = SECTIONS.map((s) => ({
|
||||
value: s,
|
||||
label: colConfigs?.find((c) => c.key === s)?.title ?? s,
|
||||
@@ -214,8 +241,11 @@ export default function DashboardPage() {
|
||||
<DashboardHeader
|
||||
quarter={QUARTER}
|
||||
stats={stats}
|
||||
activeStatus={activeStatus}
|
||||
onStatusChange={setActiveStatus}
|
||||
activeFilters={activeFilters}
|
||||
issueFilterActive={issueFilterActive}
|
||||
onToggleAll={handleToggleAll}
|
||||
onToggleStatus={handleToggleStatus}
|
||||
onToggleIssue={handleToggleIssue}
|
||||
onOpenDetailWindow={() => { openDetailWindow(); }}
|
||||
onOpenTaskManager={() => setShowTaskManager(true)}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user