- 해시 ?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
83 lines
3.3 KiB
TypeScript
83 lines
3.3 KiB
TypeScript
/* =============================================================================
|
|
* 프로젝트 목록 계산(92장) — 개요 숫자와 프로젝트 탭 거르기가 같은 규칙을 씀.
|
|
* 화면 부품을 부르지 않는 순수 함수만 둠(시험이 바로 불러 씀).
|
|
* ========================================================================== */
|
|
|
|
import type { ProjectItem, WorkflowStageState } from "./B01_Dashboard_Api_Fetch";
|
|
|
|
/** 워크플로 단계 수 — 파일 입력 · 지형 · 노선 · 종횡단 · 상세 · 수량 · 견적 */
|
|
export const STEP_COUNT = 7;
|
|
|
|
/** 거르기 값 — 해시 `filter=` 로도 들어옴 */
|
|
export type ProjectFilter = "all" | "failed" | "stale" | `stage-${number}`;
|
|
|
|
/** 표 「단계」 열과 같은 값 — 지금 하는 단계 번호 */
|
|
export function projectStep(project: ProjectItem): number {
|
|
const step = project.workflow_state?.current_stage ?? project.workflow_stage;
|
|
return Math.min(Math.max(step, 0), STEP_COUNT - 1);
|
|
}
|
|
|
|
export function hasStageState(project: ProjectItem, state: WorkflowStageState["state"]): boolean {
|
|
return (project.workflow_state?.stages ?? []).some((stage) => stage.state === state);
|
|
}
|
|
|
|
export function parseFilter(raw: string | null): ProjectFilter {
|
|
if (raw === "failed" || raw === "stale") return raw;
|
|
const found = /^stage-(\d+)$/.exec(raw ?? "");
|
|
return found && Number(found[1]) < STEP_COUNT ? (`stage-${Number(found[1])}` as const) : "all";
|
|
}
|
|
|
|
export function matchesFilter(project: ProjectItem, filter: ProjectFilter): boolean {
|
|
if (filter === "all") return true;
|
|
if (filter === "failed") return hasStageState(project, "FAILED");
|
|
if (filter === "stale") return hasStageState(project, "STALE");
|
|
return projectStep(project) === Number(filter.slice("stage-".length));
|
|
}
|
|
|
|
/** 이름 · 지역 · 발주처 · 작성자 · 사업번호에 모든 단어가 들어간 프로젝트 */
|
|
export function matchesQuery(project: ProjectItem, text: string): boolean {
|
|
const words = text.toLowerCase().split(/\s+/).filter(Boolean);
|
|
if (!words.length) return true;
|
|
const hay = [
|
|
project.name,
|
|
project.region,
|
|
project.client_org,
|
|
project.owner_name,
|
|
project.project_number,
|
|
project.road_type,
|
|
]
|
|
.join(" ")
|
|
.toLowerCase();
|
|
return words.every((word) => hay.includes(word));
|
|
}
|
|
|
|
export interface ProjectSummary {
|
|
total: number;
|
|
/** 단계 번호별 프로젝트 수 (길이 STEP_COUNT) */
|
|
byStep: number[];
|
|
failed: number;
|
|
stale: number;
|
|
}
|
|
|
|
export function summarizeProjects(projects: ProjectItem[]): ProjectSummary {
|
|
const byStep = Array.from({ length: STEP_COUNT }, () => 0);
|
|
for (const project of projects) byStep[projectStep(project)] += 1;
|
|
return {
|
|
total: projects.length,
|
|
byStep,
|
|
failed: projects.filter((p) => hasStageState(p, "FAILED")).length,
|
|
stale: projects.filter((p) => hasStageState(p, "STALE")).length,
|
|
};
|
|
}
|
|
|
|
/** 진행 현황에 보일 단계 — 실패 → 재확인 필요 → 지금 단계 순 */
|
|
export function progressStage(project: ProjectItem): WorkflowStageState | null {
|
|
const stages = project.workflow_state?.stages ?? [];
|
|
return (
|
|
stages.find((stage) => stage.state === "FAILED") ??
|
|
stages.find((stage) => stage.state === "STALE") ??
|
|
stages.find((stage) => stage.stage_no === projectStep(project)) ??
|
|
null
|
|
);
|
|
}
|