- 해시 ?tab= 탭 줄(B08 단추 꼴) · 고른 탭 자료만 받음 · 설정 카드는 탭 줄 위 · 회사 고르기 알약은 탭 줄 오른쪽 하나 - 회사 없음 진입 두 곳(router · M02 Layer)은 ?tab=company · 회사 관리자 프로젝트 목록 한 번만 받음 - 개요 숫자 카드(단계별 · 실패 · 재확인 · 가입 대기 · 보관함 · 리소스) · 프로젝트 표 칸 보강 · 찾기 · 거르기 · 쪽 넘김 · 진행 현황 - 회사 정보 칸(사업자번호 · 대표 · 상태) · 팀원 읽기 · 자산 표 · 내 계정 마지막 로그인 · 시스템 리소스 7·30·90일 · 활성 추이 · 전체 회사 · 사용자 · 로그 쪽 · 고객 문의 - 시험 test_b01_dashboard_tabs 신규 · test_b01_settings_cards 고침 Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MgChuAcyBqUkhHMTXomQC7
137 lines
4.3 KiB
TypeScript
137 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);
|
|
|
|
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(),
|
|
];
|
|
}
|