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:
EENE Dashboard
2026-06-06 00:33:58 +09:00
parent 288e05f691
commit d14ff1997c
3 changed files with 291 additions and 149 deletions

View File

@@ -1,142 +1,186 @@
import { useState } from 'react';
import { isDetailWindowOpen } from '../../lib/dualMonitor';
import { DualMonitorIcon, PlusIcon, UsersIcon } from './HeaderIcons';
interface Stats {
total: number;
inProgress: number;
review: number;
done: number;
issues: number;
}
interface DashboardHeaderProps {
quarter: string;
stats: Stats;
activeStatus: string;
onStatusChange: (status: string) => void;
onOpenDetailWindow: () => void | Promise<void>;
onOpenTaskManager: () => void;
}
const STAT_ACCENT = {
: 'text-[#ffdb3a]',
IN_PROGRESS: 'text-[#10b981]',
REVIEW: 'text-[#ff9f0a]',
DONE: 'text-[#b0b0b0]',
ISSUES: 'text-[#ff5252]',
} as const;
export function DashboardHeader({
quarter,
stats,
activeStatus,
onStatusChange,
onOpenDetailWindow,
onOpenTaskManager,
}: DashboardHeaderProps) {
const [detailViewActive, setDetailViewActive] = useState(isDetailWindowOpen);
const quarterLabel = quarter.replace(/^(\d{4})-Q(\d)$/, '$1 $2분기 업무');
const handleOpenDetailWindow = () => {
void Promise.resolve(onOpenDetailWindow()).then(() => {
setDetailViewActive(isDetailWindowOpen());
});
};
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 },
];
return (
<header className="dashboard-header-bar shrink-0">
<div className="side-left-group min-w-0 shrink-0">
<span className="side-title-main main_tit flex shrink-0 items-center gap-[10px] text-[20px] font-bold tracking-[-0.5px] text-[#bad8ca]">
<span></span>
<span>|</span>
<span>People Growth Hub</span>
</span>
<button type="button" title="팀 현황" className="team-status-btn-new">
<UsersIcon size={16} />
</button>
</div>
<div className="header-stats-bar side-polygon-stats">
<div className="poly-stat-item" style={{ fontSize: '18px', gap: '8px' }}>
<span className="poly-stat-quarter header-stat-text">{quarterLabel}</span>
<span className="poly-stat-bullet header-stat-text">·</span>
{statItems.map((item, index) => (
<span key={item.statusKey} className="contents">
{(index === 1 || index === 4) && <StatDivider />}
<StatClick
label={item.label}
value={item.value}
statusKey={item.statusKey}
activeStatus={activeStatus}
accent={STAT_ACCENT[item.statusKey]}
onClick={onStatusChange}
/>
</span>
))}
</div>
</div>
<div className="side-right-actions shrink-0">
<button type="button" onClick={onOpenTaskManager} title="신규 프로젝트 추가" className="header-action-btn-new">
<PlusIcon size={16} />
</button>
<button
type="button"
onClick={handleOpenDetailWindow}
title="듀얼뷰"
className={`header-view-btn-new ${detailViewActive ? 'active' : ''}`}
>
<DualMonitorIcon size={16} />
</button>
</div>
</header>
);
}
function StatDivider() {
return <div className="poly-stat-divider" aria-hidden />;
}
interface StatClickProps {
label: string;
value: number;
statusKey: keyof typeof STAT_ACCENT;
activeStatus: string;
accent: string;
onClick: (key: string) => void;
}
function StatClick({ label, value, statusKey, activeStatus, accent, onClick }: StatClickProps) {
const isActive = activeStatus === statusKey;
const handleActivate = () => onClick(isActive ? '전체' : statusKey);
return (
<span
role="button"
tabIndex={0}
onClick={handleActivate}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
handleActivate();
}
}}
className={`poly-click-stat header-stat-text ${isActive ? 'active' : ''}`}
style={{ cursor: 'pointer', padding: '2px 6px', borderRadius: '4px' }}
>
{label}{' '}
<span className={`poly-stat-val ${accent}`}>{value}</span>
<span className="poly-stat-unit"> </span>
</span>
);
}
import { useState } from 'react';
import { isDetailWindowOpen } from '../../lib/dualMonitor';
import {
FILTER_ALL,
isStatusChipActive,
type CoreStatusFilter,
} from '../../lib/statusFilters';
import { DualMonitorIcon, PlusIcon, UsersIcon } from './HeaderIcons';
interface Stats {
total: number;
inProgress: number;
review: number;
done: number;
issues: number;
}
interface DashboardHeaderProps {
quarter: string;
stats: Stats;
activeFilters: string[];
issueFilterActive: boolean;
onToggleAll: () => void;
onToggleStatus: (key: CoreStatusFilter) => void;
onToggleIssue: () => void;
onOpenDetailWindow: () => void | Promise<void>;
onOpenTaskManager: () => void;
}
const STAT_ACCENT = {
: '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분기 업무');
const handleOpenDetailWindow = () => {
void Promise.resolve(onOpenDetailWindow()).then(() => {
setDetailViewActive(isDetailWindowOpen());
});
};
const statItems: Array<{
label: string;
value: number;
statusKey: StatKey;
onClick: () => void;
isActive: boolean;
}> = [
{
label: '전체',
value: stats.total,
statusKey: '전체',
onClick: onToggleAll,
isActive: isStatusChipActive(FILTER_ALL, activeFilters, issueFilterActive),
},
{
label: '진행',
value: stats.inProgress,
statusKey: 'IN_PROGRESS',
onClick: () => onToggleStatus('IN_PROGRESS'),
isActive: isStatusChipActive('IN_PROGRESS', activeFilters, issueFilterActive),
},
{
label: '보류',
value: stats.review,
statusKey: 'REVIEW',
onClick: () => onToggleStatus('REVIEW'),
isActive: isStatusChipActive('REVIEW', activeFilters, issueFilterActive),
},
{
label: '완료',
value: stats.done,
statusKey: 'DONE',
onClick: () => onToggleStatus('DONE'),
isActive: isStatusChipActive('DONE', activeFilters, issueFilterActive),

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

View File

@@ -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)}
/>