226 lines
7.6 KiB
TypeScript
226 lines
7.6 KiB
TypeScript
import { useEffect, useMemo, useState } from 'react';
|
|
import { useQuery } from '@tanstack/react-query';
|
|
import { DndContext, PointerSensor, useSensor, useSensors } from '@dnd-kit/core';
|
|
import { apiClient } from '../lib/apiClient';
|
|
import { useTasks } from '../hooks/useTasks';
|
|
import { useBoardReferenceDate } from '../hooks/useBoardReferenceDate';
|
|
import { DashboardHeader } from '../components/dashboard/DashboardHeader';
|
|
import { DummyDepartmentColumn } from '../components/dummy/DummyDepartmentColumn';
|
|
import { HubColumn } from '../components/dashboard/HubColumn';
|
|
import { BoardConnectors } from '../components/dashboard/BoardConnectors';
|
|
import { isProjectTask, isRoutineTask } from '../lib/taskType';
|
|
import { hasVisibleIssue } from '../lib/taskIssues';
|
|
import {
|
|
DEFAULT_STATUS_FILTERS,
|
|
taskMatchesStatusFilters,
|
|
toggleAllFilter,
|
|
toggleCoreFilter,
|
|
type CoreStatusFilter,
|
|
} from '../lib/statusFilters';
|
|
import { LEGACY_COLUMN_KEYS, SECTIONS } from '../lib/sections';
|
|
import {
|
|
BOARD_SLOT_ORDER,
|
|
BOARD_SLOTS,
|
|
getBoardSlot,
|
|
slotSectionLabel,
|
|
taskBelongsToBoardSlot,
|
|
} from '../lib/boardLayout';
|
|
import '../styles/dummy-board.css';
|
|
|
|
function mergeCardOrders(primary: string | null | undefined, extras: (string | null | undefined)[]): string[] {
|
|
const merged: string[] = [];
|
|
const push = (raw: string | null | undefined) => {
|
|
if (!raw) return;
|
|
try {
|
|
const ids = JSON.parse(raw) as string[];
|
|
for (const id of ids) {
|
|
if (!merged.includes(id)) merged.push(id);
|
|
}
|
|
} catch {
|
|
/* ignore */
|
|
}
|
|
};
|
|
push(primary);
|
|
extras.forEach(push);
|
|
return merged;
|
|
}
|
|
|
|
export default function DummyDashboardPage() {
|
|
const [activeFilters, setActiveFilters] = useState<string[]>([...DEFAULT_STATUS_FILTERS]);
|
|
const [filtersBeforeIssue, setFiltersBeforeIssue] = useState<string[]>([...DEFAULT_STATUS_FILTERS]);
|
|
const [issueFilterActive, setIssueFilterActive] = useState(false);
|
|
const [teamPanelOpen, setTeamPanelOpen] = useState(false);
|
|
const [columnOrders, setColumnOrders] = useState<Record<string, string[]>>({});
|
|
const { referenceDate, setReferenceDate, quarter } = useBoardReferenceDate();
|
|
|
|
const { data: tasks = [], isLoading } = useTasks({ quarter });
|
|
|
|
const { data: colConfigs } = useQuery({
|
|
queryKey: ['columns', 'all', ...SECTIONS],
|
|
queryFn: async () => {
|
|
const results = await Promise.all(
|
|
SECTIONS.map(async (s) => {
|
|
const main = await apiClient.get(`/columns/${encodeURIComponent(s)}`).then((r) => r.data);
|
|
const legacy = await Promise.all(
|
|
LEGACY_COLUMN_KEYS[s].map((key) =>
|
|
apiClient.get(`/columns/${encodeURIComponent(key)}`).then((r) => r.data).catch(() => null),
|
|
),
|
|
);
|
|
const cardOrder = JSON.stringify(
|
|
mergeCardOrders(main.cardOrder, legacy.map((l) => l?.cardOrder)),
|
|
);
|
|
return { key: s, ...main, cardOrder };
|
|
}),
|
|
);
|
|
return results;
|
|
},
|
|
staleTime: 0,
|
|
});
|
|
|
|
useEffect(() => {
|
|
if (!colConfigs) return;
|
|
setColumnOrders((prev) => {
|
|
const next = { ...prev };
|
|
BOARD_SLOTS.forEach((slot) => {
|
|
const label = slotSectionLabel(slot);
|
|
if (next[label]) return;
|
|
if (!slot.sectionKey) return;
|
|
const cfg = colConfigs.find((c) => c.key === slot.sectionKey);
|
|
if (cfg?.cardOrder) {
|
|
try {
|
|
next[label] = JSON.parse(cfg.cardOrder);
|
|
} catch {
|
|
/* ignore */
|
|
}
|
|
}
|
|
});
|
|
return next;
|
|
});
|
|
}, [colConfigs]);
|
|
|
|
useEffect(() => {
|
|
BOARD_SLOTS.forEach((slot) => {
|
|
const label = slotSectionLabel(slot);
|
|
const ids = tasks.filter((t) => taskBelongsToBoardSlot(t, slot)).map((t) => t.id);
|
|
setColumnOrders((prev) => {
|
|
const existing = prev[label] ?? [];
|
|
const merged = [
|
|
...existing.filter((id) => ids.includes(id)),
|
|
...ids.filter((id) => !existing.includes(id)),
|
|
];
|
|
if (JSON.stringify(merged) === JSON.stringify(existing)) return prev;
|
|
return { ...prev, [label]: merged };
|
|
});
|
|
});
|
|
}, [tasks]);
|
|
|
|
const filtered = tasks.filter((t) => {
|
|
if (issueFilterActive) return hasVisibleIssue(t);
|
|
return taskMatchesStatusFilters(t, activeFilters);
|
|
});
|
|
|
|
const boardProjectTasks = useMemo(
|
|
() =>
|
|
filtered.filter(
|
|
(t) =>
|
|
isProjectTask(t.taskType) &&
|
|
BOARD_SLOTS.some((slot) => taskBelongsToBoardSlot(t, slot)),
|
|
),
|
|
[filtered],
|
|
);
|
|
|
|
const stats = useMemo(
|
|
() => ({
|
|
total: boardProjectTasks.length,
|
|
inProgress: boardProjectTasks.filter((t) => t.status === 'IN_PROGRESS').length,
|
|
review: boardProjectTasks.filter(
|
|
(t) => t.status === 'REVIEW' || t.status === 'CANCELLED' || t.status === 'TODO',
|
|
).length,
|
|
done: boardProjectTasks.filter((t) => t.status === 'DONE').length,
|
|
issues: boardProjectTasks.filter((t) => hasVisibleIssue(t)).length,
|
|
}),
|
|
[boardProjectTasks],
|
|
);
|
|
|
|
const routineTasks = useMemo(
|
|
() => filtered.filter((t) => isRoutineTask(t.taskType)),
|
|
[filtered],
|
|
);
|
|
|
|
const sensors = useSensors(
|
|
useSensor(PointerSensor, { activationConstraint: { distance: 9999 } }),
|
|
);
|
|
|
|
const renderDeptSlot = (slotId: typeof BOARD_SLOT_ORDER[number]) => {
|
|
const slot = getBoardSlot(slotId);
|
|
const label = slotSectionLabel(slot);
|
|
return (
|
|
<DummyDepartmentColumn
|
|
key={slot.id}
|
|
slot={slot}
|
|
tasks={filtered.filter((t) => taskBelongsToBoardSlot(t, slot))}
|
|
orderedIds={columnOrders[label] ?? []}
|
|
/>
|
|
);
|
|
};
|
|
|
|
if (isLoading) {
|
|
return (
|
|
<div className="flex h-screen items-center justify-center bg-[#e9eef2]">
|
|
<div className="text-lg text-gray-400">데이터를 불러오는 중...</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="app-shell dummy-board-page dummy-board-page--2slots">
|
|
<div className="app-main relative flex h-screen min-w-0 flex-col overflow-hidden bg-[#e9eef2]">
|
|
<DashboardHeader
|
|
quarter={quarter}
|
|
referenceDate={referenceDate}
|
|
onReferenceDateChange={setReferenceDate}
|
|
stats={stats}
|
|
activeFilters={activeFilters}
|
|
issueFilterActive={issueFilterActive}
|
|
onToggleAll={() => {
|
|
setIssueFilterActive(false);
|
|
setActiveFilters((prev) => toggleAllFilter(prev));
|
|
}}
|
|
onToggleStatus={(key: CoreStatusFilter) => {
|
|
setIssueFilterActive(false);
|
|
setActiveFilters((prev) => toggleCoreFilter(prev, key));
|
|
}}
|
|
onToggleIssue={() => {
|
|
if (issueFilterActive) {
|
|
setIssueFilterActive(false);
|
|
setActiveFilters([...filtersBeforeIssue]);
|
|
return;
|
|
}
|
|
setFiltersBeforeIssue([...activeFilters]);
|
|
setIssueFilterActive(true);
|
|
}}
|
|
onOpenDetailWindow={() => {}}
|
|
onOpenTaskManager={() => {}}
|
|
teamPanelOpen={teamPanelOpen}
|
|
onToggleTeamPanel={() => setTeamPanelOpen((v) => !v)}
|
|
/>
|
|
|
|
<main className="board-main dummy-board-main dummy-board-main--preview">
|
|
<div className="page-content">
|
|
<DndContext sensors={sensors}>
|
|
<div className="board-layout">
|
|
{renderDeptSlot('hrm')}
|
|
{renderDeptSlot('hrd')}
|
|
<HubColumn routineTasks={routineTasks} quarter={quarter} referenceDate={referenceDate} />
|
|
{renderDeptSlot('ex')}
|
|
{renderDeptSlot('ga')}
|
|
<BoardConnectors style="reference" />
|
|
</div>
|
|
</DndContext>
|
|
</div>
|
|
</main>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|