feat: polish reject history UI and enhance WIP filter interactions

This commit is contained in:
egg
2026-02-22 11:54:51 +08:00
parent 9687deb9ad
commit 7bf9e33cd5
35 changed files with 3054 additions and 1085 deletions

View File

@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-02-22

View File

@@ -0,0 +1,79 @@
## Context
`WIP 即時概況` 現在使用 4 個文字輸入框WORKORDER/LOT ID/PACKAGE/TYPE搭配 `/api/wip/meta/search` 即時建議。此模式在多條件操作時需要頻繁輸入,且首次進頁不會先拿到完整候選值。需求要改成可模糊搜尋的下拉清單,並新增 `FIRSTNAME``WAFERDESC` 兩個篩選維度,且篩選選項來源以快取為主。
## Goals / Non-Goals
**Goals**
- 將 WIP 概況篩選改成下拉可搜尋(參考設備即時概況「機台」篩選互動)
- 新增 `Wafer LOT(FIRSTNAME)``Wafer Type(WAFERDESC)` 篩選
- 所有篩選候選值可由快取一次取得,並支援首次載入預先填充
- 既有 summary/matrix/hold 查詢都能吃到新舊篩選條件
**Non-Goals**
- 不改 WIP Detail 頁面的篩選 UI仍維持現有 autocomplete
- 不移除既有 `/api/wip/meta/search`(保留向下相容)
- 不變更 WIP 指標計算邏輯(僅改篩選方式與欄位)
## Decisions
### D1: 新增 `GET /api/wip/meta/filter-options` 做「一次取齊」篩選選項
API 回傳:
- `workorders`
- `lotids`
- `packages`
- `types`
- `firstnames`
- `waferdescs`
資料來源優先順序:
1. WIP 快取衍生搜尋索引(`_get_wip_search_index`
2. WIP 快取快照(必要時)
3. Oracle fallback僅快取不可用時
此設計讓前端在第一次查詢前就能載入完整下拉選項。
### D2: WIP 概況前端篩選改採 `MultiSelect`(可搜尋)
`frontend/src/wip-overview/components/FilterPanel.vue` 改為使用 `resource-shared/components/MultiSelect.vue`
- 6 個篩選欄位皆為可搜尋下拉
- 支援多選(內部值為陣列)
- 顯示 active chips移除 chip 會觸發重查
### D3: 篩選參數以 CSV 傳遞,服務層統一解析
API query 維持既有參數名稱(`workorder`, `lotid`, `package`, `type`)並新增:
- `firstname`
- `waferdesc`
多選由前端以逗號串接傳遞。服務層新增 CSV 解析 helper將單值/多值統一轉成條件。
### D4: 搜尋索引與快照索引擴充 Wafer 欄位
`wip_service` 的衍生索引加入:
- `FIRSTNAME`
- `WAFERDESC`
確保:
- `meta/filter-options` 可直接由索引取值
- summary/matrix/hold 可在快取路徑下高效套用 Wafer 篩選
## Risks / Trade-offs
- **參數長度風險**:多選過多時 URL 長度增加;目前以一般 dashboard 操作量可接受。
- **跨頁一致性**WIP Detail 未同步改成新 UI但後端先支持新欄位避免 overview drilldown 失真。
- **快取不可用場景**filter-options 需 fallback 查詢,首次延遲可能上升。
## Validation Plan
- 單元測試:
- `tests/test_wip_routes.py`:新增 `meta/filter-options` 與新參數傳遞驗證
- `tests/test_wip_service.py`:新增 filter-options 來源與新欄位索引輸出驗證
- `frontend/tests/wip-derive.test.js`CSV/新欄位 query 參數組裝驗證
- 手動驗證:
- 進入 `/wip-overview`,首次不查主資料也能看到下拉選項
- 套用任一新舊篩選後 summary/matrix/hold 都一致變化
- 下拉框可模糊搜尋、可多選、可清除

View File

@@ -0,0 +1,36 @@
## Why
WIP 即時概況目前使用文字輸入搭配動態 autocomplete使用者在多條件查詢時容易反覆輸入且選項不一致。改為可搜尋的下拉清單並新增 Wafer 維度篩選,可降低操作成本、提升查詢一致性,且能直接利用既有快取資料來源。
## What Changes
- 將 WIP 即時概況篩選 UI 從文字 autocomplete 改為可模糊搜尋的下拉清單(對齊設備即時概況機台篩選互動)
- 新增兩個篩選欄位:`Wafer LOT`(資料欄位 `FIRSTNAME`)、`Wafer Type`(資料欄位 `WAFERDESC`
- 新增 WIP 篩選選項 API一次回傳舊有與新增篩選欄位的候選值優先由 WIP 快取衍生索引提供
- WIP 概況查詢 APIsummary/matrix/hold納入 `firstname``waferdesc` 參數
- 前端初始化階段預先載入篩選選項(不需先觸發主查詢才有下拉選項)
## Capabilities
### New Capabilities
_(none)_
### Modified Capabilities
- `wip-overview-page`: 篩選互動改為可搜尋下拉並新增 Wafer 維度篩選,且篩選選項由快取驅動
## Impact
- **Frontend**
- `frontend/src/wip-overview/App.vue`
- `frontend/src/wip-overview/components/FilterPanel.vue`
- `frontend/src/wip-overview/style.css`
- `frontend/src/core/wip-derive.js`
- `frontend/tests/wip-derive.test.js`
- **Backend**
- `src/mes_dashboard/routes/wip_routes.py`
- `src/mes_dashboard/services/wip_service.py`
- `tests/test_wip_routes.py`
- `tests/test_wip_service.py`
- **No breaking route removal**: 既有 API 與參數仍保持相容,新增參數採選填。

View File

@@ -0,0 +1,39 @@
## MODIFIED Requirements
### Requirement: Overview page SHALL support dropdown filtering
The page SHALL provide searchable dropdown filters for WORKORDER, LOT ID, PACKAGE, TYPE, Wafer LOT, and Wafer Type.
#### Scenario: Filter options preload from cache-backed endpoint
- **WHEN** the page initializes
- **THEN** the page SHALL call `GET /api/wip/meta/filter-options`
- **THEN** dropdown options SHALL be loaded before user performs first query
- **THEN** options SHALL include `workorders`, `lotids`, `packages`, `types`, `firstnames`, and `waferdescs`
#### Scenario: Searchable dropdown interaction
- **WHEN** user opens any filter dropdown
- **THEN** the dropdown SHALL support fuzzy keyword search over loaded options
- **THEN** user SHALL be able to select one or multiple options
#### Scenario: Apply and clear filters
- **WHEN** user clicks `套用篩選`
- **THEN** all three API calls (`/api/wip/overview/summary`, `/api/wip/overview/matrix`, `/api/wip/overview/hold`) SHALL reload with selected filter values
- **WHEN** user clicks `清除篩選`
- **THEN** all filter values SHALL reset and data SHALL reload without filters
#### Scenario: Active filter chips
- **WHEN** any filter has selected values
- **THEN** selected values SHALL be displayed as removable chips
- **THEN** removing a chip SHALL trigger data reload with updated filters
### Requirement: Overview page SHALL persist filter state in URL
The page SHALL synchronize all filter state to URL query parameters as the single source of truth.
#### Scenario: URL state includes new wafer filters
- **WHEN** filters are applied
- **THEN** URL query parameters SHALL include non-empty values for `workorder`, `lotid`, `package`, `type`, `firstname`, `waferdesc`, and `status`
- **THEN** multi-select values SHALL be serialized as comma-separated strings
#### Scenario: URL state restoration on load
- **WHEN** the page is loaded with filter query parameters
- **THEN** all filter controls SHALL restore values from URL
- **THEN** data SHALL load with restored filters applied

View File

@@ -0,0 +1,25 @@
## 1. OpenSpec alignment
- [x] 1.1 Confirm modified capability scope and spec deltas for `wip-overview-page`
## 2. Backend: cache-backed filter options + new filter fields
- [x] 2.1 Add WIP service support for `FIRSTNAME` / `WAFERDESC` in cache-derived indexes and snapshot filter path
- [x] 2.2 Add cache-backed `get_wip_filter_options` service API returning workorders/lotids/packages/types/firstnames/waferdescs
- [x] 2.3 Add `GET /api/wip/meta/filter-options` route
- [x] 2.4 Extend overview query routes (`summary`, `matrix`, `hold`) to parse and pass `firstname` and `waferdesc`
- [x] 2.5 Keep backward compatibility for existing params and behavior
## 3. Frontend: WIP overview filter UX replacement
- [x] 3.1 Replace `wip-overview` filter inputs with searchable dropdowns (reuse `resource-shared/components/MultiSelect.vue`)
- [x] 3.2 Add two new filters in UI: `Wafer LOT` (`firstname`) and `Wafer Type` (`waferdesc`)
- [x] 3.3 Load filter options from `/api/wip/meta/filter-options` on initialization and bind to dropdown options
- [x] 3.4 Ensure apply/clear/chip-remove and URL sync all work with old + new filters
## 4. Tests and verification
- [x] 4.1 Update route tests for new endpoint and new query parameters
- [x] 4.2 Update service tests for filter options and new index fields
- [x] 4.3 Update frontend derive tests for URL/query param mapping
- [x] 4.4 Run targeted test commands and fix regressions

View File

@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-02-22

View File

@@ -0,0 +1,57 @@
## Context
The reject-history page is a monolithic `App.vue` (~968 lines template+script) with a co-located `style.css`. It was built quickly and works, but differs from the hold-history page (the maturity benchmark) in structure and several UI details. The hold-history page delegates to 7 sub-components and follows project-wide conventions (loading overlay, hover effects, Chinese pagination text, header refresh button).
The page imports `wip-shared/styles.css` for design tokens and global classes, and `resource-shared/components/MultiSelect.vue` for multi-select dropdowns — but also duplicates ~120 lines of MultiSelect CSS in its own `style.css`.
## Goals / Non-Goals
**Goals:**
- Match hold-history's visual baseline: loading overlay, table hover, Chinese pagination, header refresh button
- Extract App.vue into sub-components following hold-history's proven pattern
- Remove duplicated MultiSelect CSS
- Keep all existing functionality and API interactions unchanged
**Non-Goals:**
- Changing column names or data display (user explicitly excluded #1)
- Adding new features, APIs, or functional capabilities
- Migrating to Tailwind or shared-ui components (page stays on wip-shared CSS)
- Touching backend code
## Decisions
### D1: Component extraction mirrors hold-history's architecture
Extract into 5 sub-components under `frontend/src/reject-history/components/`:
| Component | Responsibility | hold-history equivalent |
|-----------|---------------|------------------------|
| `FilterPanel.vue` | Filter grid, checkboxes, action buttons, active chips | `FilterBar.vue` |
| `SummaryCards.vue` | 6 KPI cards with lane colors | `SummaryCards.vue` |
| `TrendChart.vue` | Quantity trend bar chart (vue-echarts) | `DailyTrend.vue` |
| `ParetoSection.vue` | Pareto chart + table side-by-side | `ReasonPareto.vue` |
| `DetailTable.vue` | Detail table + pagination | `DetailTable.vue` |
**Rationale**: The hold-history pattern is proven and familiar to the team. Same granularity, same naming convention.
### D2: State stays in App.vue, components receive props + emit events
App.vue keeps all reactive state (`filters`, `summary`, `trend`, `pareto`, `detail`, `loading`, etc.) and API functions. Sub-components are presentational. This matches hold-history exactly and avoids over-engineering with composables for a single-page report.
### D3: Remove duplicated MultiSelect CSS, rely on resource-shared import chain
The MultiSelect component from `resource-shared/components/MultiSelect.vue` already bundles its own styles. The ~120 lines duplicated in `reject-history/style.css` (`.multi-select`, `.multi-select-trigger`, `.multi-select-dropdown`, etc.) can be deleted.
**Risk**: If some pages import MultiSelect without importing `resource-shared/styles.css`, they break. But reject-history doesn't import resource-shared/styles.css either — the MultiSelect component uses scoped styles or injects its own. Verify before deleting.
### D4: Loading overlay uses existing wip-shared pattern
Add `<div v-if="loading.initial" class="loading-overlay"><span class="loading-spinner"></span></div>` after the `.dashboard` div, identical to hold-history. The `.loading-overlay` and `.loading-spinner` classes are already defined in `wip-shared/styles.css`.
## Risks / Trade-offs
- **[Risk] MultiSelect CSS deletion breaks styling** → Verify the MultiSelect component renders correctly after removing the duplicated CSS. If it doesn't, the component may need its own `<style scoped>` block or the import chain needs adjustment.
- **[Risk] Extraction introduces subtle regressions** → Each component boundary is a potential data-flow bug. Mitigate by keeping the extraction mechanical: cut template section → paste into component → add props/emits.
- **[Trade-off] No composable extraction** → The script logic stays in App.vue (400+ lines). This is acceptable for now — hold-history works the same way. Future refactoring can extract a `useRejectHistory` composable if needed.

View File

@@ -0,0 +1,28 @@
## Why
The reject-history page was shipped as a monolithic single-file implementation. While functional, it has visual inconsistencies with other mature report pages (hold-history, wip-overview) and is missing standard UX affordances. Aligning it now reduces user confusion when switching between report pages and improves maintainability.
## What Changes
- Add `tbody tr:hover` highlight and missing loading overlay/spinner to match hold-history baseline
- Localize pagination controls from English (Prev/Next/Page/Total) to Chinese (上一頁/下一頁/頁/共)
- Remove ~120 lines of duplicated MultiSelect CSS from `style.css` (already provided by `resource-shared/styles.css`)
- Add a "重新整理" (refresh) button in the header, consistent with hold-history
- Extract monolithic `App.vue` (~968 lines) into focused sub-components mirroring hold-history's architecture: `FilterPanel`, `SummaryCards`, `TrendChart`, `ParetoSection`, `DetailTable`
## Capabilities
### New Capabilities
_(none — no new functional capabilities are introduced)_
### Modified Capabilities
- `reject-history-page`: UI/UX polish — add loading overlay, hover effects, localized pagination, header refresh button, and modular component extraction
## Impact
- **Files modified**: `frontend/src/reject-history/App.vue`, `frontend/src/reject-history/style.css`
- **Files created**: `frontend/src/reject-history/components/FilterPanel.vue`, `SummaryCards.vue`, `TrendChart.vue`, `ParetoSection.vue`, `DetailTable.vue`
- **No API changes** — all backend endpoints remain untouched
- **No dependency changes** — continues using `vue-echarts`, `resource-shared/MultiSelect`

View File

@@ -0,0 +1,78 @@
## MODIFIED Requirements
### Requirement: Reject History page SHALL provide filterable historical query controls
The page SHALL provide a filter area for date range and major production dimensions to drive all report sections.
#### Scenario: Default filter values
- **WHEN** the page is first loaded
- **THEN** `start_date` and `end_date` SHALL default to a valid recent range
- **THEN** all other dimension filters SHALL default to empty (no restriction)
#### Scenario: Apply and clear filters
- **WHEN** user clicks "查詢"
- **THEN** summary, trend, pareto, and list sections SHALL reload with the same filter set
- **WHEN** user clicks "清除條件"
- **THEN** all filters SHALL reset to defaults and all sections SHALL reload
#### Scenario: Required core filters are present
- **WHEN** the filter panel is rendered
- **THEN** it SHALL include `start_date/end_date` time filter controls
- **THEN** it SHALL include reason filter control
- **THEN** it SHALL include `WORKCENTER_GROUP` filter control
#### Scenario: Header refresh button
- **WHEN** the page header is rendered
- **THEN** it SHALL include a "重新整理" button in the header-right area
- **WHEN** user clicks the refresh button
- **THEN** all sections SHALL reload with current filters (equivalent to "查詢")
## ADDED Requirements
### Requirement: Reject History page SHALL display a loading overlay during initial data load
The page SHALL show a full-screen loading overlay with spinner during the first data load to provide clear feedback.
#### Scenario: Loading overlay on initial mount
- **WHEN** the page first mounts and `loadAllData` begins
- **THEN** a loading overlay with spinner SHALL be displayed over the page content
- **WHEN** all initial API responses complete
- **THEN** the overlay SHALL be hidden
#### Scenario: Subsequent queries do not show overlay
- **WHEN** the user triggers a re-query after initial load
- **THEN** no full-screen overlay SHALL appear (inline loading states are sufficient)
### Requirement: Detail table rows SHALL highlight on hover
The detail table and pareto table rows SHALL visually respond to mouse hover for improved readability.
#### Scenario: Row hover in detail table
- **WHEN** user hovers over a row in the detail table
- **THEN** the row background SHALL change to a subtle highlight color
#### Scenario: Row hover in pareto table
- **WHEN** user hovers over a row in the pareto summary table
- **THEN** the row background SHALL change to a subtle highlight color
### Requirement: Pagination controls SHALL use Chinese labels
The detail list pagination SHALL display controls in Chinese to match the rest of the page language.
#### Scenario: Pagination button labels
- **WHEN** the pagination controls are rendered
- **THEN** the previous-page button SHALL display "上一頁"
- **THEN** the next-page button SHALL display "下一頁"
- **THEN** the page info text SHALL use Chinese formatting (e.g., "第 1 / 5 頁 · 共 250 筆")
### Requirement: Reject History page SHALL be structured as modular sub-components
The page template SHALL delegate sections to focused sub-components, following the hold-history architecture pattern.
#### Scenario: Component decomposition
- **WHEN** the page source is examined
- **THEN** the filter panel SHALL be a separate `FilterPanel.vue` component
- **THEN** the KPI summary cards SHALL be a separate `SummaryCards.vue` component
- **THEN** the trend chart SHALL be a separate `TrendChart.vue` component
- **THEN** the pareto section (chart + table) SHALL be a separate `ParetoSection.vue` component
- **THEN** the detail table with pagination SHALL be a separate `DetailTable.vue` component
#### Scenario: App.vue acts as orchestrator
- **WHEN** the page runs
- **THEN** `App.vue` SHALL hold all reactive state and API logic
- **THEN** sub-components SHALL receive data via props and communicate via events

View File

@@ -0,0 +1,26 @@
## 1. Quick visual fixes (no component extraction needed)
- [x] 1.1 Add `tbody tr:hover` background rule to `style.css` for `.detail-table` and `.pareto-table`
- [x] 1.2 Localize pagination: change "Prev" → "上一頁", "Next" → "下一頁", "Page X / Y · Total Z" → "第 X / Y 頁 · 共 Z 筆"
- [x] 1.3 Add loading overlay + spinner after `.dashboard` div (`<div v-if="loading.initial" class="loading-overlay">`)
- [x] 1.4 Add "重新整理" button in header-right area, wired to `applyFilters`
- [x] 1.5 Remove duplicated MultiSelect CSS (~120 lines of `.multi-select-*` rules) from `style.css`; verify MultiSelect still renders correctly
## 2. Extract sub-components from App.vue
- [x] 2.1 Create `components/FilterPanel.vue` — extract filter grid, checkbox row, action buttons, and active-filter chips section; props: `filters`, `options`, `loading`, `activeFilterChips`; emits: `apply`, `clear`, `remove-chip`, `export-csv`, `pareto-scope-toggle`
- [x] 2.2 Create `components/SummaryCards.vue` — extract `.summary-row` section; props: `cards`
- [x] 2.3 Create `components/TrendChart.vue` — extract trend chart `.card` section with ECharts registration and chart option computed internally; props: `items`, `loading`
- [x] 2.4 Create `components/ParetoSection.vue` — extract pareto chart + table `.card` section with ECharts registration and chart option computed internally; props: `items`, `detailReason`, `loading`; emits: `reason-click`
- [x] 2.5 Create `components/DetailTable.vue` — extract detail table + pagination; props: `items`, `pagination`, `loading`; emits: `go-to-page`
## 3. Rewire App.vue as orchestrator
- [x] 3.1 Replace inline template sections with sub-component tags, passing props and wiring emits
- [x] 3.2 Move ECharts `use()` registration and chart computed properties into their respective chart components
- [x] 3.3 Verify all interactions work: filter apply/clear, pareto click → detail filter, pagination, CSV export, refresh button
## 4. Verify and build
- [x] 4.1 Run `vite build` and confirm no compilation errors
- [x] 4.2 Visually verify: loading overlay, table hover, Chinese pagination, refresh button, pareto interaction, filter chips