260710_1
This commit is contained in:
@@ -14,6 +14,23 @@ export interface DashboardUser {
|
||||
status: string;
|
||||
}
|
||||
|
||||
export interface WorkflowStageState {
|
||||
stage_no: number;
|
||||
stage_key: string;
|
||||
state: "NOT_STARTED" | "IN_PROGRESS" | "COMPLETE" | "FAILED" | "STALE";
|
||||
progress_percent: number;
|
||||
params?: any | null;
|
||||
message?: string | null;
|
||||
started_at?: string | null;
|
||||
completed_at?: string | null;
|
||||
}
|
||||
|
||||
export interface WorkflowState {
|
||||
project_id: string;
|
||||
current_stage: number;
|
||||
stages: WorkflowStageState[];
|
||||
}
|
||||
|
||||
export interface ProjectItem {
|
||||
id: string;
|
||||
company_id?: number | null;
|
||||
@@ -27,6 +44,7 @@ export interface ProjectItem {
|
||||
owner_name?: string | null;
|
||||
workflow_stage: number;
|
||||
progress_percent: number;
|
||||
workflow_state?: WorkflowState;
|
||||
updated_at?: string | null;
|
||||
}
|
||||
|
||||
@@ -348,3 +366,9 @@ export function deleteProjectAutomation(automationId: number): Promise<unknown>
|
||||
export function executeProjectAutomation(automationId: number): Promise<unknown> {
|
||||
return request(`/dashboard/automations/${automationId}/execute`, { method: "POST" });
|
||||
}
|
||||
|
||||
export function fetchProjectWorkflowState(projectId: string): Promise<WorkflowState> {
|
||||
return request(`/projects/${projectId}/workflow-state`, {
|
||||
method: "GET",
|
||||
}) as Promise<WorkflowState>;
|
||||
}
|
||||
|
||||
@@ -80,6 +80,67 @@ async def update_user_profile(user_id: int, data: dict[str, Any]) -> dict[str, A
|
||||
return await get_dashboard_me(user_id)
|
||||
|
||||
|
||||
async def get_workflow_states_for_projects(
|
||||
cursor: aiomysql.DictCursor, project_ids: list[str]
|
||||
) -> dict[str, dict[str, Any]]:
|
||||
if not project_ids:
|
||||
return {}
|
||||
format_strings = ",".join(["%s"] * len(project_ids))
|
||||
await cursor.execute(
|
||||
f"""
|
||||
SELECT project_id, stage_no, stage_key, state, progress_percent, params, message,
|
||||
DATE_FORMAT(started_at, '%%Y-%%m-%%dT%%H:%%i:%%s') AS started_at,
|
||||
DATE_FORMAT(completed_at, '%%Y-%%m-%%dT%%H:%%i:%%s') AS completed_at
|
||||
FROM project_workflow_stages
|
||||
WHERE project_id IN ({format_strings})
|
||||
ORDER BY project_id, stage_no ASC
|
||||
""",
|
||||
tuple(project_ids),
|
||||
)
|
||||
rows = await cursor.fetchall()
|
||||
|
||||
project_stages = {}
|
||||
for r in rows:
|
||||
pid = r["project_id"]
|
||||
if pid not in project_stages:
|
||||
project_stages[pid] = []
|
||||
params_val = None
|
||||
if r.get("params"):
|
||||
try:
|
||||
params_val = (
|
||||
json.loads(r["params"]) if isinstance(r["params"], str) else r["params"]
|
||||
)
|
||||
except Exception:
|
||||
params_val = r["params"]
|
||||
project_stages[pid].append(
|
||||
{
|
||||
"stage_no": r["stage_no"],
|
||||
"stage_key": r["stage_key"],
|
||||
"state": r["state"],
|
||||
"progress_percent": r["progress_percent"],
|
||||
"params": params_val,
|
||||
"message": r["message"],
|
||||
"started_at": r["started_at"],
|
||||
"completed_at": r["completed_at"],
|
||||
}
|
||||
)
|
||||
|
||||
result = {}
|
||||
for pid in project_ids:
|
||||
stages = project_stages.get(pid, [])
|
||||
current_stage = 0
|
||||
for stage in stages:
|
||||
if stage["stage_no"] == 0:
|
||||
continue
|
||||
prev_stage = stages[stage["stage_no"] - 1]
|
||||
if prev_stage["state"] == "COMPLETE":
|
||||
current_stage = stage["stage_no"]
|
||||
else:
|
||||
break
|
||||
result[pid] = {"current_stage": current_stage, "stages": stages}
|
||||
return result
|
||||
|
||||
|
||||
async def list_user_projects(user_id: int) -> list[dict[str, Any]]:
|
||||
pool = get_db_pool()
|
||||
async with pool.acquire() as connection, connection.cursor(aiomysql.DictCursor) as cursor:
|
||||
@@ -90,7 +151,15 @@ async def list_user_projects(user_id: int) -> list[dict[str, Any]]:
|
||||
ORDER BY updated_at DESC, created_at DESC""",
|
||||
(user_id,),
|
||||
)
|
||||
return [_project_row(row) for row in await cursor.fetchall()]
|
||||
rows = await cursor.fetchall()
|
||||
pids = [r["id"] for r in rows]
|
||||
states = await get_workflow_states_for_projects(cursor, pids)
|
||||
result = []
|
||||
for r in rows:
|
||||
p_row = _project_row(r)
|
||||
p_row["workflow_state"] = states.get(r["id"], {"current_stage": 0, "stages": []})
|
||||
result.append(p_row)
|
||||
return result
|
||||
|
||||
|
||||
async def list_company_projects(company_id: int) -> list[dict[str, Any]]:
|
||||
@@ -105,7 +174,15 @@ async def list_company_projects(company_id: int) -> list[dict[str, Any]]:
|
||||
ORDER BY p.updated_at DESC, p.created_at DESC""",
|
||||
(company_id,),
|
||||
)
|
||||
return [_project_row(row) for row in await cursor.fetchall()]
|
||||
rows = await cursor.fetchall()
|
||||
pids = [r["id"] for r in rows]
|
||||
states = await get_workflow_states_for_projects(cursor, pids)
|
||||
result = []
|
||||
for r in rows:
|
||||
p_row = _project_row(r)
|
||||
p_row["workflow_state"] = states.get(r["id"], {"current_stage": 0, "stages": []})
|
||||
result.append(p_row)
|
||||
return result
|
||||
|
||||
|
||||
async def list_all_projects() -> list[dict[str, Any]]:
|
||||
@@ -119,7 +196,15 @@ async def list_all_projects() -> list[dict[str, Any]]:
|
||||
WHERE p.deleted_at IS NULL
|
||||
ORDER BY p.updated_at DESC, p.created_at DESC"""
|
||||
)
|
||||
return [_project_row(row) for row in await cursor.fetchall()]
|
||||
rows = await cursor.fetchall()
|
||||
pids = [r["id"] for r in rows]
|
||||
states = await get_workflow_states_for_projects(cursor, pids)
|
||||
result = []
|
||||
for r in rows:
|
||||
p_row = _project_row(r)
|
||||
p_row["workflow_state"] = states.get(r["id"], {"current_stage": 0, "stages": []})
|
||||
result.append(p_row)
|
||||
return result
|
||||
|
||||
|
||||
async def get_project(project_id: str) -> dict[str, Any] | None:
|
||||
|
||||
@@ -380,12 +380,22 @@ function workflow(project: ProjectItem): HTMLElement {
|
||||
];
|
||||
const box = document.createElement("div");
|
||||
box.className = "b01-dashboard__workflow";
|
||||
|
||||
const stages = project.workflow_state?.stages;
|
||||
|
||||
routes.forEach((route, index) => {
|
||||
const button = document.createElement("button");
|
||||
button.type = "button";
|
||||
button.className = "b01-dashboard__step";
|
||||
button.textContent = `B${String(index + 3).padStart(2, "0")}`;
|
||||
const enabled = index + 1 <= Math.max(activeStage, 1);
|
||||
|
||||
let enabled = false;
|
||||
if (stages && stages.length > 0) {
|
||||
enabled = index === 0 || stages[index - 1]?.state === "COMPLETE";
|
||||
} else {
|
||||
enabled = index + 1 <= Math.max(activeStage, 1);
|
||||
}
|
||||
|
||||
button.disabled = !enabled;
|
||||
if (enabled) {
|
||||
button.classList.add("is-enabled");
|
||||
@@ -394,6 +404,23 @@ function workflow(project: ProjectItem): HTMLElement {
|
||||
navigateTo(route);
|
||||
});
|
||||
}
|
||||
|
||||
if (stages && stages[index]) {
|
||||
const state = stages[index].state;
|
||||
button.classList.add(`state-${state.toLowerCase()}`);
|
||||
if (state === "STALE") {
|
||||
button.title = "Stale (하위 단계 변경으로 무효화됨)";
|
||||
} else if (state === "FAILED") {
|
||||
button.title = "Failed (실패)";
|
||||
} else if (state === "COMPLETE") {
|
||||
button.title = "Complete (완료)";
|
||||
} else if (state === "IN_PROGRESS") {
|
||||
button.title = "In Progress (진행 중)";
|
||||
} else {
|
||||
button.title = "Not Started (미실행)";
|
||||
}
|
||||
}
|
||||
|
||||
box.append(button);
|
||||
});
|
||||
return box;
|
||||
|
||||
Reference in New Issue
Block a user