fix(reject-history): resolve SPEC→WORKCENTER via lookup and fix Pareto reactivity

SQL CTEs now join on SPECNAME instead of WORKCENTERNAME to resolve
correct WORKCENTER/GROUP from DW_MES_SPEC_WORKCENTER_V, fixing cases
where the raw WORKCENTERNAME was mismatched (e.g. W/B-END with 成型_料).
WORKCENTER_GROUP filter converts groups→specs via cached mapping before
querying. Pareto chart now recalculates on legend toggle by spreading
the ECharts selected object to trigger Vue reactivity.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
egg
2026-02-22 14:31:02 +08:00
parent 5f6e2a5ce0
commit 33ef58f833
5 changed files with 99 additions and 21 deletions

View File

@@ -568,7 +568,9 @@ function onTrendDateClick(dateStr) {
}
function onTrendLegendChange(selected) {
trendLegendSelected.value = selected;
// Spread to create a new object — ECharts reuses the same internal reference,
// and Vue's ref setter skips trigger when Object.is(old, new) is true.
trendLegendSelected.value = { ...selected };
page.value = 1;
updateUrlState();
void loadListOnly();

View File

@@ -32,6 +32,7 @@ _CACHE = {
'workcenter_mapping': None, # Dict {workcentername: {group, sequence}}
'workcenter_to_short': None, # Dict {workcentername: short_name}
'spec_order_mapping': None, # Dict {spec_name_upper: spec_order}
'spec_workcenter_mapping': None, # Dict {spec_name_upper: {workcenter, group, sequence}}
'last_refresh': None,
'is_loading': False,
}
@@ -162,6 +163,33 @@ def get_spec_order_mapping(force_refresh: bool = False) -> Dict[str, int]:
return {}
def get_spec_workcenter_mapping(force_refresh: bool = False) -> Dict[str, Dict[str, Any]]:
"""Get SPEC -> {workcenter, group, sequence} mapping.
Returns:
Dict mapping normalized SPEC name (uppercase) to workcenter info.
"""
_ensure_cache_loaded(force_refresh)
return _CACHE.get('spec_workcenter_mapping') or {}
def get_specs_for_groups(groups: List[str]) -> List[str]:
"""Get list of SPEC names that belong to specified workcenter groups.
Args:
groups: List of WORKCENTER_GROUP names
Returns:
List of normalized SPEC names (uppercase) belonging to those groups.
"""
mapping = get_spec_workcenter_mapping()
if not mapping:
return []
target = {g.strip().upper() for g in groups if g}
return [spec for spec, info in mapping.items()
if info['group'].strip().upper() in target]
# ============================================================
# Cache Management
# ============================================================
@@ -181,6 +209,7 @@ def get_cache_status() -> Dict[str, Any]:
'workcenter_groups_count': len(_CACHE.get('workcenter_groups') or []),
'workcenter_mapping_count': len(_CACHE.get('workcenter_mapping') or {}),
'spec_order_mapping_count': len(_CACHE.get('spec_order_mapping') or {}),
'spec_workcenter_mapping_count': len(_CACHE.get('spec_workcenter_mapping') or {}),
}
@@ -231,19 +260,22 @@ def _load_cache() -> bool:
# Load workcenter groups - prioritize SPEC_WORKCENTER_V
wc_groups, wc_mapping, wc_short = _load_workcenter_data()
spec_order_mapping = _load_spec_order_mapping_from_spec()
spec_wc_mapping = _load_spec_workcenter_mapping()
with _CACHE_LOCK:
_CACHE['workcenter_groups'] = wc_groups
_CACHE['workcenter_mapping'] = wc_mapping
_CACHE['workcenter_to_short'] = wc_short
_CACHE['spec_order_mapping'] = spec_order_mapping
_CACHE['spec_workcenter_mapping'] = spec_wc_mapping
_CACHE['last_refresh'] = datetime.now()
_CACHE['is_loading'] = False
logger.info(
f"Filter cache refreshed: {len(wc_groups or [])} groups, "
f"{len(wc_mapping or {})} workcenters, "
f"{len(spec_order_mapping or {})} specs"
f"{len(spec_order_mapping or {})} specs, "
f"{len(spec_wc_mapping or {})} spec-wc mappings"
)
return True
@@ -418,6 +450,37 @@ def _load_spec_order_mapping_from_spec() -> Dict[str, int]:
return {}
def _load_spec_workcenter_mapping() -> Dict[str, Dict[str, Any]]:
"""Load SPEC -> {workcenter, group, sequence} mapping from SPEC_WORKCENTER_V."""
try:
sql = f"""
SELECT SPEC, WORK_CENTER, WORK_CENTER_GROUP, WORKCENTERSEQUENCE_GROUP
FROM {SPEC_WORKCENTER_VIEW}
WHERE SPEC IS NOT NULL AND WORK_CENTER IS NOT NULL
"""
df = read_sql_df(sql)
if df is None or df.empty:
return {}
mapping: Dict[str, Dict[str, Any]] = {}
for _, row in df.iterrows():
spec = _normalize_spec_name(row.get('SPEC'))
if not spec:
continue
seq = _safe_sort_value(row.get('WORKCENTERSEQUENCE_GROUP'))
prev = mapping.get(spec)
if prev is None or seq < prev['sequence']:
mapping[spec] = {
'workcenter': str(row['WORK_CENTER']).strip(),
'group': str(row['WORK_CENTER_GROUP']).strip(),
'sequence': seq,
}
return mapping
except Exception as exc:
logger.error(f"Failed to load SPEC_WORKCENTER mapping from SPEC_WORKCENTER_V: {exc}")
return {}
def _extract_workcenter_data_from_df(df):
"""Extract workcenter groups and mapping from DataFrame.

View File

@@ -206,6 +206,13 @@ def _build_where_clause(
normalized_categories = sorted({_normalize_text(v) for v in (categories or []) if _normalize_text(v)})
if normalized_wc_groups:
from mes_dashboard.services.filter_cache import get_specs_for_groups
specs_in_groups = get_specs_for_groups(normalized_wc_groups)
if specs_in_groups:
# Specs in cache are uppercase; use UPPER() for case-insensitive match
builder.add_in_condition("UPPER(b.SPECNAME)", specs_in_groups)
else:
# Fallback: cache not ready or no specs found for these groups
builder.add_in_condition("b.WORKCENTER_GROUP", normalized_wc_groups)
if normalized_packages:
builder.add_in_condition("b.PRODUCTLINENAME", normalized_packages)

View File

@@ -16,16 +16,19 @@
-- 3) MOVEIN_QTY is de-duplicated at event level by HISTORYMAINLINEID.
-- 4) SPEC_WORKCENTER_V is pre-aggregated to avoid row multiplication.
WITH workcenter_map AS (
WITH spec_map AS (
SELECT
WORK_CENTER,
SPEC,
MIN(WORK_CENTER) KEEP (
DENSE_RANK FIRST ORDER BY WORKCENTERSEQUENCE_GROUP
) AS WORK_CENTER,
MIN(WORK_CENTER_GROUP) KEEP (
DENSE_RANK FIRST ORDER BY WORKCENTERSEQUENCE_GROUP
) AS WORKCENTER_GROUP,
MIN(WORKCENTERSEQUENCE_GROUP) AS WORKCENTERSEQUENCE_GROUP
FROM DWH.DW_MES_SPEC_WORKCENTER_V
WHERE WORK_CENTER IS NOT NULL
GROUP BY WORK_CENTER
WHERE SPEC IS NOT NULL
GROUP BY SPEC
),
reject_raw AS (
SELECT
@@ -36,9 +39,9 @@ reject_raw AS (
NVL(TRIM(c.PJ_TYPE), '(NA)') AS PJ_TYPE,
NVL(TRIM(c.PRODUCTLINENAME), '(NA)') AS PRODUCTLINENAME,
NVL(TRIM(c.OBJECTTYPE), '(NA)') AS SCRAP_OBJECTTYPE,
NVL(TRIM(r.WORKCENTERNAME), '(NA)') AS WORKCENTERNAME,
NVL(TRIM(wm.WORKCENTER_GROUP), NVL(TRIM(r.WORKCENTERNAME), '(NA)')) AS WORKCENTER_GROUP,
NVL(wm.WORKCENTERSEQUENCE_GROUP, 999) AS WORKCENTERSEQUENCE_GROUP,
NVL(TRIM(sm.WORK_CENTER), NVL(TRIM(r.WORKCENTERNAME), '(NA)')) AS WORKCENTERNAME,
NVL(TRIM(sm.WORKCENTER_GROUP), NVL(TRIM(r.WORKCENTERNAME), '(NA)')) AS WORKCENTER_GROUP,
NVL(sm.WORKCENTERSEQUENCE_GROUP, 999) AS WORKCENTERSEQUENCE_GROUP,
NVL(TRIM(r.SPECNAME), '(NA)') AS SPECNAME,
NVL(TRIM(r.EQUIPMENTNAME), '(NA)') AS EQUIPMENTNAME,
NVL(
@@ -73,8 +76,8 @@ reject_raw AS (
FROM DWH.DW_MES_LOTREJECTHISTORY r
LEFT JOIN DWH.DW_MES_CONTAINER c
ON c.CONTAINERID = r.CONTAINERID
LEFT JOIN workcenter_map wm
ON wm.WORK_CENTER = r.WORKCENTERNAME
LEFT JOIN spec_map sm
ON sm.SPEC = TRIM(r.SPECNAME)
WHERE r.TXNDATE >= TO_DATE(:start_date, 'YYYY-MM-DD')
AND r.TXNDATE < TO_DATE(:end_date, 'YYYY-MM-DD') + 1
),

View File

@@ -5,16 +5,19 @@
-- :start_date - Start date (YYYY-MM-DD)
-- :end_date - End date (YYYY-MM-DD)
WITH workcenter_map AS (
WITH spec_map AS (
SELECT
WORK_CENTER,
SPEC,
MIN(WORK_CENTER) KEEP (
DENSE_RANK FIRST ORDER BY WORKCENTERSEQUENCE_GROUP
) AS WORK_CENTER,
MIN(WORK_CENTER_GROUP) KEEP (
DENSE_RANK FIRST ORDER BY WORKCENTERSEQUENCE_GROUP
) AS WORKCENTER_GROUP,
MIN(WORKCENTERSEQUENCE_GROUP) AS WORKCENTERSEQUENCE_GROUP
FROM DWH.DW_MES_SPEC_WORKCENTER_V
WHERE WORK_CENTER IS NOT NULL
GROUP BY WORK_CENTER
WHERE SPEC IS NOT NULL
GROUP BY SPEC
),
reject_raw AS (
SELECT
@@ -29,9 +32,9 @@ reject_raw AS (
NVL(TRIM(c.PRODUCTNAME), '(NA)') AS PRODUCTNAME,
NVL(TRIM(c.PRODUCTLINENAME), '(NA)') AS PRODUCTLINENAME,
NVL(TRIM(c.OBJECTTYPE), '(NA)') AS SCRAP_OBJECTTYPE,
NVL(TRIM(r.WORKCENTERNAME), '(NA)') AS WORKCENTERNAME,
NVL(TRIM(wm.WORKCENTER_GROUP), NVL(TRIM(r.WORKCENTERNAME), '(NA)')) AS WORKCENTER_GROUP,
NVL(wm.WORKCENTERSEQUENCE_GROUP, 999) AS WORKCENTERSEQUENCE_GROUP,
NVL(TRIM(sm.WORK_CENTER), NVL(TRIM(r.WORKCENTERNAME), '(NA)')) AS WORKCENTERNAME,
NVL(TRIM(sm.WORKCENTER_GROUP), NVL(TRIM(r.WORKCENTERNAME), '(NA)')) AS WORKCENTER_GROUP,
NVL(sm.WORKCENTERSEQUENCE_GROUP, 999) AS WORKCENTERSEQUENCE_GROUP,
NVL(TRIM(r.SPECNAME), '(NA)') AS SPECNAME,
NVL(TRIM(r.EQUIPMENTNAME), '(NA)') AS EQUIPMENTNAME,
NVL(
@@ -67,8 +70,8 @@ reject_raw AS (
FROM DWH.DW_MES_LOTREJECTHISTORY r
LEFT JOIN DWH.DW_MES_CONTAINER c
ON c.CONTAINERID = r.CONTAINERID
LEFT JOIN workcenter_map wm
ON wm.WORK_CENTER = r.WORKCENTERNAME
LEFT JOIN spec_map sm
ON sm.SPEC = TRIM(r.SPECNAME)
WHERE r.TXNDATE >= TO_DATE(:start_date, 'YYYY-MM-DD')
AND r.TXNDATE < TO_DATE(:end_date, 'YYYY-MM-DD') + 1
),