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) { 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; page.value = 1;
updateUrlState(); updateUrlState();
void loadListOnly(); void loadListOnly();

View File

@@ -32,6 +32,7 @@ _CACHE = {
'workcenter_mapping': None, # Dict {workcentername: {group, sequence}} 'workcenter_mapping': None, # Dict {workcentername: {group, sequence}}
'workcenter_to_short': None, # Dict {workcentername: short_name} 'workcenter_to_short': None, # Dict {workcentername: short_name}
'spec_order_mapping': None, # Dict {spec_name_upper: spec_order} 'spec_order_mapping': None, # Dict {spec_name_upper: spec_order}
'spec_workcenter_mapping': None, # Dict {spec_name_upper: {workcenter, group, sequence}}
'last_refresh': None, 'last_refresh': None,
'is_loading': False, 'is_loading': False,
} }
@@ -162,6 +163,33 @@ def get_spec_order_mapping(force_refresh: bool = False) -> Dict[str, int]:
return {} 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 # Cache Management
# ============================================================ # ============================================================
@@ -181,6 +209,7 @@ def get_cache_status() -> Dict[str, Any]:
'workcenter_groups_count': len(_CACHE.get('workcenter_groups') or []), 'workcenter_groups_count': len(_CACHE.get('workcenter_groups') or []),
'workcenter_mapping_count': len(_CACHE.get('workcenter_mapping') or {}), 'workcenter_mapping_count': len(_CACHE.get('workcenter_mapping') or {}),
'spec_order_mapping_count': len(_CACHE.get('spec_order_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 # Load workcenter groups - prioritize SPEC_WORKCENTER_V
wc_groups, wc_mapping, wc_short = _load_workcenter_data() wc_groups, wc_mapping, wc_short = _load_workcenter_data()
spec_order_mapping = _load_spec_order_mapping_from_spec() spec_order_mapping = _load_spec_order_mapping_from_spec()
spec_wc_mapping = _load_spec_workcenter_mapping()
with _CACHE_LOCK: with _CACHE_LOCK:
_CACHE['workcenter_groups'] = wc_groups _CACHE['workcenter_groups'] = wc_groups
_CACHE['workcenter_mapping'] = wc_mapping _CACHE['workcenter_mapping'] = wc_mapping
_CACHE['workcenter_to_short'] = wc_short _CACHE['workcenter_to_short'] = wc_short
_CACHE['spec_order_mapping'] = spec_order_mapping _CACHE['spec_order_mapping'] = spec_order_mapping
_CACHE['spec_workcenter_mapping'] = spec_wc_mapping
_CACHE['last_refresh'] = datetime.now() _CACHE['last_refresh'] = datetime.now()
_CACHE['is_loading'] = False _CACHE['is_loading'] = False
logger.info( logger.info(
f"Filter cache refreshed: {len(wc_groups or [])} groups, " f"Filter cache refreshed: {len(wc_groups or [])} groups, "
f"{len(wc_mapping or {})} workcenters, " 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 return True
@@ -418,6 +450,37 @@ def _load_spec_order_mapping_from_spec() -> Dict[str, int]:
return {} 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): def _extract_workcenter_data_from_df(df):
"""Extract workcenter groups and mapping from DataFrame. """Extract workcenter groups and mapping from DataFrame.

View File

@@ -206,7 +206,14 @@ def _build_where_clause(
normalized_categories = sorted({_normalize_text(v) for v in (categories or []) if _normalize_text(v)}) normalized_categories = sorted({_normalize_text(v) for v in (categories or []) if _normalize_text(v)})
if normalized_wc_groups: if normalized_wc_groups:
builder.add_in_condition("b.WORKCENTER_GROUP", 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: if normalized_packages:
builder.add_in_condition("b.PRODUCTLINENAME", normalized_packages) builder.add_in_condition("b.PRODUCTLINENAME", normalized_packages)
if reason_name_filters: if reason_name_filters:

View File

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

View File

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