feat(透视表): 工序分布包含在库/完成 + 增加时间筛选
- wip-matrix 去掉状态过滤,全量分布,工序包含「在库」(生产完成) - 后端加 since/until 按任务接手/创建时间过滤 - 前端 MatrixBoard 加 RangePicker 日期筛选 + 全部清除
This commit is contained in:
@ -104,10 +104,14 @@ async def user_operations_detail(
|
|||||||
@router.get("/wip-matrix", response_model=list[WipMatrixRow])
|
@router.get("/wip-matrix", response_model=list[WipMatrixRow])
|
||||||
async def wip_matrix(
|
async def wip_matrix(
|
||||||
dimension: str = Query("assignee", description="聚合维度: assignee(人员) / task_name(工序)"),
|
dimension: str = Query("assignee", description="聚合维度: assignee(人员) / task_name(工序)"),
|
||||||
|
since: str | None = Query(None, description="起始日期 ISO"),
|
||||||
|
until: str | None = Query(None, description="截止日期 ISO"),
|
||||||
db: AsyncSession = Depends(get_db),
|
db: AsyncSession = Depends(get_db),
|
||||||
):
|
):
|
||||||
"""在制品分布透视表 — 规格型号 × 人员/工序 的设备数量交叉聚合"""
|
"""生产分布透视表 — 规格型号 × 人员/工序 的设备数量交叉聚合(含在库/完成)"""
|
||||||
return await get_wip_matrix(db, dimension=dimension)
|
since_dt = datetime.fromisoformat(since) if since else None
|
||||||
|
until_dt = datetime.fromisoformat(until) if until else None
|
||||||
|
return await get_wip_matrix(db, dimension=dimension, since=since_dt, until=until_dt)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/people-workload", response_model=list[PersonWorkload])
|
@router.get("/people-workload", response_model=list[PersonWorkload])
|
||||||
|
|||||||
@ -607,14 +607,19 @@ async def get_user_operations(
|
|||||||
async def get_wip_matrix(
|
async def get_wip_matrix(
|
||||||
db: AsyncSession,
|
db: AsyncSession,
|
||||||
dimension: str = "assignee",
|
dimension: str = "assignee",
|
||||||
|
since: datetime | None = None,
|
||||||
|
until: datetime | None = None,
|
||||||
) -> list[WipMatrixRow]:
|
) -> list[WipMatrixRow]:
|
||||||
"""在制品交叉聚合:Y=规格型号,X=人员 或 工序,单元格=设备数量。
|
"""生产分布透视表:Y=规格型号,X=人员 或 工序,单元格=设备数量。
|
||||||
|
|
||||||
|
覆盖全部任务状态(含已完成/在库),工序分布能看到「在库」(生产完成)。
|
||||||
|
since/until 按任务接手/创建时间过滤。
|
||||||
|
|
||||||
dimension:
|
dimension:
|
||||||
- assignee: 按任务负责人聚合(dimension_key 为中文姓名)
|
- assignee: 按任务负责人聚合(dimension_key 为中文姓名)
|
||||||
- task_name: 按工序名聚合(dimension_key 为工序名,附主负责人)
|
- task_name: 按工序名聚合(dimension_key 为工序名,附主负责人)
|
||||||
"""
|
"""
|
||||||
from app.models.task import Task, TASK_STATUS_WIP, TASK_STATUS_PENDING
|
from app.models.task import Task
|
||||||
from app.models.product import Product
|
from app.models.product import Product
|
||||||
|
|
||||||
dim_expr = Task.task_name if dimension == "task_name" else Task.assignee_id
|
dim_expr = Task.task_name if dimension == "task_name" else Task.assignee_id
|
||||||
@ -627,10 +632,13 @@ async def get_wip_matrix(
|
|||||||
func.array_agg(func.distinct(Task.assignee_id)),
|
func.array_agg(func.distinct(Task.assignee_id)),
|
||||||
)
|
)
|
||||||
.join(Task, Task.product_id == Product.id)
|
.join(Task, Task.product_id == Product.id)
|
||||||
.where(Task.status.in_([TASK_STATUS_WIP, TASK_STATUS_PENDING]))
|
|
||||||
.group_by(Product.spec_model, dim_expr)
|
.group_by(Product.spec_model, dim_expr)
|
||||||
.order_by(Product.spec_model, dim_expr)
|
.order_by(Product.spec_model, dim_expr)
|
||||||
)
|
)
|
||||||
|
if since:
|
||||||
|
stmt = stmt.where(func.coalesce(Task.received_at, Task.created_at) >= since)
|
||||||
|
if until:
|
||||||
|
stmt = stmt.where(func.coalesce(Task.received_at, Task.created_at) <= until)
|
||||||
result = await db.execute(stmt)
|
result = await db.execute(stmt)
|
||||||
rows = result.all()
|
rows = result.all()
|
||||||
|
|
||||||
|
|||||||
@ -3,21 +3,29 @@
|
|||||||
* Y 轴 = 规格型号,X 轴 = 人员 或 工序,单元格 = 该交叉点的在制品设备数
|
* Y 轴 = 规格型号,X 轴 = 人员 或 工序,单元格 = 该交叉点的在制品设备数
|
||||||
*/
|
*/
|
||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { Radio, Table } from "antd";
|
import { Radio, Table, DatePicker, Button } from "antd";
|
||||||
|
import type { Dayjs } from "dayjs";
|
||||||
import { fetchWipMatrix, type WipMatrixRow } from "../services/dashboardApi";
|
import { fetchWipMatrix, type WipMatrixRow } from "../services/dashboardApi";
|
||||||
|
|
||||||
|
const { RangePicker } = DatePicker;
|
||||||
|
|
||||||
export default function MatrixBoard() {
|
export default function MatrixBoard() {
|
||||||
const [dimension, setDimension] = useState<"assignee" | "task_name">("assignee");
|
const [dimension, setDimension] = useState<"assignee" | "task_name">("assignee");
|
||||||
|
const [range, setRange] = useState<[Dayjs, Dayjs] | null>(null);
|
||||||
const [data, setData] = useState<WipMatrixRow[]>([]);
|
const [data, setData] = useState<WipMatrixRow[]>([]);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
fetchWipMatrix(dimension)
|
fetchWipMatrix(
|
||||||
|
dimension,
|
||||||
|
range ? range[0].startOf("day").toISOString() : undefined,
|
||||||
|
range ? range[1].endOf("day").toISOString() : undefined,
|
||||||
|
)
|
||||||
.then(setData)
|
.then(setData)
|
||||||
.catch(() => setData([]))
|
.catch(() => setData([]))
|
||||||
.finally(() => setLoading(false));
|
.finally(() => setLoading(false));
|
||||||
}, [dimension]);
|
}, [dimension, range]);
|
||||||
|
|
||||||
// 🔧 核心:扁平数组 → 动态交叉表(规格型号为行、dimension_key 为列)
|
// 🔧 核心:扁平数组 → 动态交叉表(规格型号为行、dimension_key 为列)
|
||||||
const { columns, dataSource } = useMemo(() => {
|
const { columns, dataSource } = useMemo(() => {
|
||||||
@ -67,13 +75,23 @@ export default function MatrixBoard() {
|
|||||||
<div className="mb-4 flex flex-wrap items-center justify-between gap-3">
|
<div className="mb-4 flex flex-wrap items-center justify-between gap-3">
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-xl font-bold text-gray-800">📊 WIP 分布矩阵</h2>
|
<h2 className="text-xl font-bold text-gray-800">📊 WIP 分布矩阵</h2>
|
||||||
<p className="mt-1 text-sm text-gray-500">在制品分布透视表:规格型号 × {dimension === "assignee" ? "人员" : "工序"} · 单元格为设备数量</p>
|
<p className="mt-1 text-sm text-gray-500">生产分布透视表:规格型号 × {dimension === "assignee" ? "人员" : "工序"} · 含在库/完成</p>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<RangePicker
|
||||||
|
size="small"
|
||||||
|
value={range as any}
|
||||||
|
onChange={(dates) => setRange(dates as [Dayjs, Dayjs] | null)}
|
||||||
|
style={{ width: 240 }}
|
||||||
|
placeholder={["开始日期", "结束日期"]}
|
||||||
|
/>
|
||||||
|
{range && <Button size="small" onClick={() => setRange(null)}>全部</Button>}
|
||||||
<Radio.Group value={dimension} onChange={(e) => setDimension(e.target.value)} optionType="button" buttonStyle="solid">
|
<Radio.Group value={dimension} onChange={(e) => setDimension(e.target.value)} optionType="button" buttonStyle="solid">
|
||||||
<Radio.Button value="assignee">按人员分布</Radio.Button>
|
<Radio.Button value="assignee">按人员分布</Radio.Button>
|
||||||
<Radio.Button value="task_name">按工序分布</Radio.Button>
|
<Radio.Button value="task_name">按工序分布</Radio.Button>
|
||||||
</Radio.Group>
|
</Radio.Group>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="rounded-xl bg-white p-4 shadow-sm">
|
<div className="rounded-xl bg-white p-4 shadow-sm">
|
||||||
<Table
|
<Table
|
||||||
|
|||||||
@ -184,8 +184,13 @@ export async function fetchOperationDetail(
|
|||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchWipMatrix(dimension: "assignee" | "task_name"): Promise<WipMatrixRow[]> {
|
export async function fetchWipMatrix(
|
||||||
const { data } = await api.get<WipMatrixRow[]>("/dashboard/wip-matrix", { params: { dimension } });
|
dimension: "assignee" | "task_name", since?: string, until?: string,
|
||||||
|
): Promise<WipMatrixRow[]> {
|
||||||
|
const params: Record<string, string> = { dimension };
|
||||||
|
if (since) params.since = since;
|
||||||
|
if (until) params.until = until;
|
||||||
|
const { data } = await api.get<WipMatrixRow[]>("/dashboard/wip-matrix", { params });
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user