From 33ef58f83307e5d14b62e6a5310077e2c77b672a Mon Sep 17 00:00:00 2001 From: egg Date: Sun, 22 Feb 2026 14:31:02 +0800 Subject: [PATCH] =?UTF-8?q?fix(reject-history):=20resolve=20SPEC=E2=86=92W?= =?UTF-8?q?ORKCENTER=20via=20lookup=20and=20fix=20Pareto=20reactivity?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- frontend/src/reject-history/App.vue | 4 +- src/mes_dashboard/services/filter_cache.py | 65 ++++++++++++++++++- .../services/reject_history_service.py | 9 ++- .../sql/reject_history/performance_daily.sql | 21 +++--- .../reject_history/performance_daily_lot.sql | 21 +++--- 5 files changed, 99 insertions(+), 21 deletions(-) diff --git a/frontend/src/reject-history/App.vue b/frontend/src/reject-history/App.vue index 9494bcd..3b6645c 100644 --- a/frontend/src/reject-history/App.vue +++ b/frontend/src/reject-history/App.vue @@ -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(); diff --git a/src/mes_dashboard/services/filter_cache.py b/src/mes_dashboard/services/filter_cache.py index 4e9458e..e90654b 100644 --- a/src/mes_dashboard/services/filter_cache.py +++ b/src/mes_dashboard/services/filter_cache.py @@ -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. diff --git a/src/mes_dashboard/services/reject_history_service.py b/src/mes_dashboard/services/reject_history_service.py index 6bf78f9..d9e763d 100644 --- a/src/mes_dashboard/services/reject_history_service.py +++ b/src/mes_dashboard/services/reject_history_service.py @@ -206,7 +206,14 @@ def _build_where_clause( normalized_categories = sorted({_normalize_text(v) for v in (categories or []) if _normalize_text(v)}) 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: builder.add_in_condition("b.PRODUCTLINENAME", normalized_packages) if reason_name_filters: diff --git a/src/mes_dashboard/sql/reject_history/performance_daily.sql b/src/mes_dashboard/sql/reject_history/performance_daily.sql index 4a65514..e4958c1 100644 --- a/src/mes_dashboard/sql/reject_history/performance_daily.sql +++ b/src/mes_dashboard/sql/reject_history/performance_daily.sql @@ -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 ), diff --git a/src/mes_dashboard/sql/reject_history/performance_daily_lot.sql b/src/mes_dashboard/sql/reject_history/performance_daily_lot.sql index 034f8bb..a19704d 100644 --- a/src/mes_dashboard/sql/reject_history/performance_daily_lot.sql +++ b/src/mes_dashboard/sql/reject_history/performance_daily_lot.sql @@ -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 ),