EENE Dashboard upload to Gitea
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
225
frontend/src/pages/DummyDashboardPage.tsx
Normal file
225
frontend/src/pages/DummyDashboardPage.tsx
Normal file
@@ -0,0 +1,225 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user