Files
eene_dashboard/frontend/src/pages/DummyDashboardPage.tsx
EENE Dashboard b3f2da203b EENE Dashboard upload to Gitea
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-17 16:59:34 +09:00

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