Files
Aislo/B01_Dashboard/B01_Dashboard_UI_ProjectsTab.ts
T
eomsangdonandClaude Sonnet 5.5 22266508d4 fix(B01): 항목별 탭 화면 다듬음 — 회사 정보 칸 줄맞춤 · 리소스 기간 글 · 프로젝트 표 단계 막대 두 줄 · 찾기 줄 정렬 (92-1·92-2)
- 정보 칸 라벨 · 값 한 쌍씩 묶음 · 리소스 그래프 글이 고른 기간을 따름 · 프로젝트 표 가로 여백 줄임
- 화면 검증(ORCA) 권한 셋 · 밝음 · 어두움 · 390px 끝 — PLAN 92장 검증완료

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MgChuAcyBqUkhHMTXomQC7
2026-10-08 14:10:03 +09:00

138 lines
4.3 KiB
TypeScript

/* =============================================================================
* 「프로젝트」 탭(92장) — 프로젝트 표(찾기 · 거르기 · 쪽 넘김) + 진행 현황 + 임시 보관함.
* ========================================================================== */
import { ROUTES } from "@config/config_frontend";
import {
createButton,
createInputField,
createSelectField,
createTag,
type TagVariant,
} from "@ui/ui_template_elements";
import { section, text } from "@ui/ui_template_general_blocks";
import { workflowSteps } from "../A00_Common/b_page_scaffold";
import { currentQuery, navigateTo } from "../A00_Common/router";
import type { ProjectItem, WorkflowStageState } from "./B01_Dashboard_Api_Fetch";
import { formatDate, formatTime, L, stackedCell } from "./B01_Dashboard_UI_Common";
import { createPagedTable } from "./B01_Dashboard_UI_Paging";
import type { DashboardState } from "./B01_Dashboard_UI_Page";
import { projectHeaders, projectRow } from "./B01_Dashboard_UI_Projects";
import {
matchesFilter,
matchesQuery,
parseFilter,
progressStage,
STEP_COUNT,
type ProjectFilter,
} from "./B01_Dashboard_UI_ProjectStats";
import { buildTempUploadSection } from "./B01_Dashboard_UI_TempUpload";
const STATE_TAG: Record<WorkflowStageState["state"], TagVariant> = {
NOT_STARTED: "neutral",
IN_PROGRESS: "accent",
COMPLETE: "success",
FAILED: "danger",
STALE: "warning",
};
function filterOptions(): { value: string; text: string }[] {
const steps = workflowSteps();
return [
{ value: "all", text: L("B01_Pj_FilterAll") },
{ value: "failed", text: L("B01_Ov_Failed") },
{ value: "stale", text: L("B01_Ov_Stale") },
...Array.from({ length: STEP_COUNT }, (_, i) => ({
value: `stage-${i}`,
text: `${L("B01_Pj_Stage")} · ${steps[i]}`,
})),
];
}
function progressRow(project: ProjectItem): HTMLElement[] {
const stage = progressStage(project);
if (!stage)
return [
text(project.name),
text(null),
text(null),
text(null),
text(null),
text(null),
text(null),
];
const when = (value?: string | null): HTMLElement =>
value ? stackedCell(formatDate(value), formatTime(value)) : text(null);
return [
text(project.name),
text(workflowSteps()[stage.stage_no] ?? stage.stage_key),
createTag(L(`B01_Pj_State_${stage.state}`), STATE_TAG[stage.state]),
text(`${Math.round(stage.progress_percent)}%`),
text(stage.message),
when(stage.started_at),
when(stage.completed_at),
];
}
function progressHeaders(): string[] {
return [
L("B01_Dashboard_Table_Project"),
L("B01_Pj_Stage"),
L("B01_Pj_State"),
L("B01_Pj_Percent"),
L("B01_Pj_Message"),
L("B01_Pj_Started"),
L("B01_Pj_Done"),
];
}
/** `projects` = 역할별 프로젝트 목록 · 거르기 처음 값은 해시 `filter=`(개요에서 눌러 올 때) */
export function buildProjectsTab(state: DashboardState, projects: ProjectItem[]): HTMLElement[] {
const user = state.user;
const list = createPagedTable(
projectHeaders(),
(p: ProjectItem) => projectRow(p, user),
projects,
);
const progress = createPagedTable(progressHeaders(), progressRow, projects);
list.root.classList.add("b01-dashboard__paged--dense");
let filter: ProjectFilter = parseFilter(currentQuery().get("filter"));
const query = createInputField({
label: L("B01_Pj_Search"),
placeholder: L("B01_Pj_SearchHint"),
});
const filterField = createSelectField({
label: L("B01_Pj_Filter"),
options: filterOptions(),
value: filter,
onChange: (value) => {
filter = parseFilter(value);
apply();
},
});
function apply(): void {
const shown = projects.filter(
(p) => matchesFilter(p, filter) && matchesQuery(p, query.input.value),
);
list.setItems(shown);
progress.setItems(shown);
}
query.input.addEventListener("input", apply);
apply();
const filters = document.createElement("div");
filters.className = "b01-dashboard__filters";
filters.append(query.root, filterField.root);
const body = document.createElement("div");
body.append(filters, list.root);
return [
section(L("B01_Dashboard_Projects"), body, true, [
createButton({ label: "+", onClick: () => navigateTo(ROUTES.B02_PROJ_REGISTER) }),
]),
section(L("B01_Pj_Progress"), progress.root, true),
buildTempUploadSection(),
];
}