feat(reject-history): ship report page and archive openspec change

This commit is contained in:
egg
2026-02-13 20:35:52 +08:00
parent 7cbb155619
commit 05d907ac72
47 changed files with 4419 additions and 73 deletions

View File

@@ -1,9 +1,6 @@
## Purpose
Define stable requirements for field-name-consistency.
## Requirements
### Requirement: UI and Export Fields SHALL Have a Consistent Contract
The system SHALL define and apply a consistent contract among UI column labels, API keys, and export headers for report/query pages.
@@ -14,3 +11,25 @@ The system SHALL define and apply a consistent contract among UI column labels,
#### Scenario: Resource history field alignment
- **WHEN** resource history detail table shows KPI columns
- **THEN** columns required by export semantics (including Availability%) SHALL be present or explicitly mapped
### Requirement: Reject and defect metric names SHALL remain semantically consistent across UI/API/export
The system SHALL use explicit, stable names for charge-off reject and non-charge-off defect metrics across all output surfaces.
#### Scenario: UI and API key alignment
- **WHEN** summary/trend/list payloads are rendered on reject-history page
- **THEN** UI labels for reject metrics SHALL map to `REJECT_TOTAL_QTY` and related reject-rate fields
- **THEN** UI labels for defect metrics SHALL map to `DEFECT_QTY` and defect-rate fields
#### Scenario: Export header alignment
- **WHEN** reject-history CSV export is generated
- **THEN** CSV headers SHALL include both `REJECT_TOTAL_QTY` and `DEFECT_QTY`
- **THEN** header names SHALL preserve the same semantic meaning as API fields
### Requirement: Reject component columns SHALL be explicitly distinguished from defect columns
The system SHALL prevent ambiguous naming that collapses reject components and defect into a single term.
#### Scenario: Component and aggregate coexistence
- **WHEN** detailed records are presented
- **THEN** reject component fields (`REJECTQTY`, `STANDBYQTY`, `QTYTOPROCESS`, `INPROCESSQTY`, `PROCESSEDQTY`) SHALL be distinguishable from `DEFECT_QTY`
- **THEN** aggregate `REJECT_TOTAL_QTY` SHALL be clearly identified as component sum, not defect

View File

@@ -0,0 +1,117 @@
# reject-history-api Specification
## Purpose
TBD - created by archiving change reject-history-query-page. Update Purpose after archive.
## Requirements
### Requirement: Reject History API SHALL validate required query parameters
The API SHALL validate date parameters and basic paging bounds before executing database work.
#### Scenario: Missing required dates
- **WHEN** a reject-history endpoint requiring date range is called without `start_date` or `end_date`
- **THEN** the API SHALL return HTTP 400 with a descriptive validation error
#### Scenario: Invalid date order
- **WHEN** `end_date` is earlier than `start_date`
- **THEN** the API SHALL return HTTP 400 and SHALL NOT run SQL queries
### Requirement: Reject History API SHALL provide summary metrics endpoint
The API SHALL provide aggregated summary metrics for the selected filter context.
#### Scenario: Summary response payload
- **WHEN** `GET /api/reject-history/summary` is called with valid filters
- **THEN** response SHALL be `{ success: true, data: { ... } }`
- **THEN** data SHALL include `MOVEIN_QTY`, `REJECT_TOTAL_QTY`, `DEFECT_QTY`, `REJECT_RATE_PCT`, `DEFECT_RATE_PCT`, `REJECT_SHARE_PCT`, `AFFECTED_LOT_COUNT`, and `AFFECTED_WORKORDER_COUNT`
### Requirement: Reject History API SHALL support yield-exclusion policy toggle
The API SHALL support excluding or including policy-marked scrap reasons through a shared query parameter.
#### Scenario: Default policy mode
- **WHEN** reject-history endpoints are called without `include_excluded_scrap`
- **THEN** `include_excluded_scrap` SHALL default to `false`
- **THEN** rows mapped to `ERP_PJ_WIP_SCRAP_REASONS_EXCLUDE.ENABLE_FLAG='Y'` SHALL be excluded from yield-related calculations
#### Scenario: Explicitly include policy-marked scrap
- **WHEN** `include_excluded_scrap=true` is provided
- **THEN** policy-marked rows SHALL be included in summary/trend/pareto/list/export calculations
- **THEN** API response `meta` SHALL include the effective `include_excluded_scrap` value
#### Scenario: Invalid toggle value
- **WHEN** `include_excluded_scrap` is not parseable as boolean
- **THEN** the API SHALL return HTTP 400 with a descriptive validation error
### Requirement: Reject History API SHALL provide trend endpoint
The API SHALL return time-series trend data for quantity and rate metrics.
#### Scenario: Trend response structure
- **WHEN** `GET /api/reject-history/trend` is called
- **THEN** response SHALL be `{ success: true, data: { items: [...] } }`
- **THEN** each trend item SHALL contain bucket date, `REJECT_TOTAL_QTY`, `DEFECT_QTY`, `REJECT_RATE_PCT`, and `DEFECT_RATE_PCT`
#### Scenario: Trend granularity
- **WHEN** `granularity` is provided as `day`, `week`, or `month`
- **THEN** the API SHALL aggregate by the requested granularity
- **THEN** invalid granularity SHALL return HTTP 400
### Requirement: Reject History API SHALL provide reason Pareto endpoint
The API SHALL return sorted reason distribution data with cumulative percentages.
#### Scenario: Pareto response payload
- **WHEN** `GET /api/reject-history/reason-pareto` is called
- **THEN** each item SHALL include `reason`, `category`, selected metric value, `pct`, and `cumPct`
- **THEN** items SHALL be sorted descending by selected metric
#### Scenario: Metric mode validation
- **WHEN** `metric_mode` is provided
- **THEN** accepted values SHALL be `reject_total` or `defect`
- **THEN** invalid `metric_mode` SHALL return HTTP 400
### Requirement: Reject History API SHALL provide paginated detail endpoint
The API SHALL return paginated detailed rows for the selected filter context.
#### Scenario: List response payload
- **WHEN** `GET /api/reject-history/list?page=1&per_page=50` is called
- **THEN** response SHALL include `{ items: [...], pagination: { page, perPage, total, totalPages } }`
- **THEN** each row SHALL include date, process dimensions, reason fields, `MOVEIN_QTY`, `REJECT_TOTAL_QTY`, `DEFECT_QTY`, and reject component columns
#### Scenario: Paging bounds
- **WHEN** `page < 1`
- **THEN** page SHALL be treated as 1
- **WHEN** `per_page > 200`
- **THEN** `per_page` SHALL be capped at 200
### Requirement: Reject History API SHALL provide CSV export endpoint
The API SHALL provide CSV export using the same filter and metric semantics as list/query APIs.
#### Scenario: Export payload consistency
- **WHEN** `GET /api/reject-history/export` is called with valid filters
- **THEN** CSV headers SHALL include both `REJECT_TOTAL_QTY` and `DEFECT_QTY`
- **THEN** export rows SHALL follow the same semantic definitions as summary/list endpoints
### Requirement: Reject History API SHALL centralize SQL in reject_history SQL directory
The service SHALL load SQL from dedicated files under `src/mes_dashboard/sql/reject_history/`.
#### Scenario: SQL file loading
- **WHEN** reject-history service executes queries
- **THEN** SQL SHALL be loaded from files in `sql/reject_history`
- **THEN** user-supplied filters SHALL be passed through bind parameters
- **THEN** user input SHALL NOT be interpolated into SQL strings directly
### Requirement: Reject History API SHALL use cached exclusion-policy source
The API SHALL read exclusion-policy reasons from cached `ERP_PJ_WIP_SCRAP_REASONS_EXCLUDE` data instead of querying Oracle on every request.
#### Scenario: Enabled exclusions only
- **WHEN** exclusion-policy data is loaded
- **THEN** only rows with `ENABLE_FLAG='Y'` SHALL be treated as active exclusions
#### Scenario: Daily full-table cache refresh
- **WHEN** exclusion cache is initialized
- **THEN** the full table SHALL be loaded and refreshed at least once per 24 hours
- **THEN** Redis SHOULD be used as shared cache when available, with in-memory fallback when unavailable
### Requirement: Reject History API SHALL apply rate limiting on expensive endpoints
The API SHALL rate-limit high-cost endpoints to protect Oracle and application resources.
#### Scenario: List and export rate limiting
- **WHEN** `/api/reject-history/list` or `/api/reject-history/export` receives excessive requests
- **THEN** configured rate limiting SHALL reject requests beyond the threshold within the time window

View File

@@ -0,0 +1,142 @@
# reject-history-page Specification
## Purpose
TBD - created by archiving change reject-history-query-page. Update Purpose after archive.
## 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
### Requirement: Reject History page SHALL expose yield-exclusion toggle control
The page SHALL let users decide whether to include policy-marked scrap in yield calculations.
#### Scenario: Default toggle state
- **WHEN** the page is first loaded
- **THEN** "納入不計良率報廢" toggle SHALL default to OFF
- **THEN** requests SHALL be sent with `include_excluded_scrap=false`
#### Scenario: Toggle affects all sections
- **WHEN** user turns ON/OFF the toggle and clicks "查詢"
- **THEN** summary, trend, pareto, and list sections SHALL reload under the selected policy mode
- **THEN** export action SHALL use the same toggle state
#### Scenario: Policy status visibility
- **WHEN** data is rendered
- **THEN** the UI SHALL show a clear badge/text indicating whether policy-marked scrap is currently excluded or included
### Requirement: Reject History page SHALL present KPI cards with split reject/defect semantics
The page SHALL display KPI cards that simultaneously show charge-off reject and non-charge-off defect metrics.
#### Scenario: KPI cards render core metrics
- **WHEN** summary data is loaded
- **THEN** cards SHALL include `MOVEIN_QTY`, `REJECT_TOTAL_QTY`, `DEFECT_QTY`, `REJECT_RATE_PCT`, `DEFECT_RATE_PCT`, `REJECT_SHARE_PCT`, `AFFECTED_LOT_COUNT`, and `AFFECTED_WORKORDER_COUNT`
- **THEN** numbers SHALL use zh-TW formatting
#### Scenario: Visual distinction for semantic lanes
- **WHEN** KPI cards are rendered
- **THEN** reject-related cards SHALL use a warm-color visual lane
- **THEN** defect-related cards SHALL use a cool-color visual lane
- **THEN** page legend/badge text SHALL explicitly indicate charge-off vs non-charge-off meaning
### Requirement: Reject History page SHALL display quantity and rate trends in separate charts
The page SHALL show both quantity trend and rate trend to avoid mixing unit scales.
#### Scenario: Quantity trend chart
- **WHEN** trend data is loaded
- **THEN** the quantity trend chart SHALL show `REJECT_TOTAL_QTY` and `DEFECT_QTY` over time
- **THEN** the chart SHALL use a shared X-axis by date bucket
#### Scenario: Rate trend chart
- **WHEN** trend data is loaded
- **THEN** the rate trend chart SHALL show `REJECT_RATE_PCT` and `DEFECT_RATE_PCT` over time
- **THEN** rate values SHALL be displayed as percentages
### Requirement: Reject History page SHALL provide reason Pareto analysis
The page SHALL provide a Pareto view for loss reasons and support downstream filtering.
#### Scenario: Pareto rendering and ordering
- **WHEN** reason Pareto data is loaded
- **THEN** items SHALL be sorted by selected metric descending
- **THEN** a cumulative percentage line SHALL be shown
#### Scenario: Default 80% cumulative display mode
- **WHEN** the page first loads Pareto
- **THEN** it SHALL default to "only cumulative top 80%" mode
- **THEN** Pareto SHALL only render categories within the cumulative 80% threshold under current filters
#### Scenario: Full Pareto toggle mode
- **WHEN** user turns OFF the 80% cumulative display mode
- **THEN** Pareto SHALL render all categories after applying current filters
- **THEN** switching mode SHALL NOT reset existing time/reason/workcenter-group filters
#### Scenario: Pareto click filtering
- **WHEN** user clicks a Pareto bar or row
- **THEN** the selected reason SHALL become an active filter chip
- **THEN** detail list SHALL reload with that reason
- **THEN** clicking the same reason again SHALL clear the reason filter
### Requirement: Reject History page SHALL show paginated detail rows
The page SHALL provide a paginated detail table for investigation and traceability.
#### Scenario: Detail columns
- **WHEN** list data is loaded
- **THEN** each row SHALL include date, workcenter group, workcenter, product dimensions, reason/category, `MOVEIN_QTY`, `REJECT_TOTAL_QTY`, `DEFECT_QTY`, and component reject columns
#### Scenario: Pagination behavior
- **WHEN** total records exceed per-page size
- **THEN** Prev/Next and page summary SHALL be shown
- **THEN** changing any filter SHALL reset page to 1
### Requirement: Reject History page SHALL support CSV export from current filter context
The page SHALL allow users to export records using the exact active filters.
#### Scenario: Export with current filters
- **WHEN** user clicks "匯出 CSV"
- **THEN** export request SHALL include the current filter state and active reason filter
- **THEN** downloaded file SHALL contain both `REJECT_TOTAL_QTY` and `DEFECT_QTY`
### Requirement: Reject History page SHALL provide robust feedback states
The page SHALL provide loading, empty, and error states without breaking interactions.
#### Scenario: Initial loading
- **WHEN** first query is running
- **THEN** a loading overlay or skeleton SHALL be visible until required data sections are ready
#### Scenario: API failure
- **WHEN** any section API fails
- **THEN** a visible error banner SHALL be shown
- **THEN** already loaded sections SHALL remain interactive
#### Scenario: Empty dataset
- **WHEN** query returns no rows
- **THEN** chart and table areas SHALL show explicit empty-state messages
### Requirement: Reject History page SHALL maintain responsive visual hierarchy
The page SHALL keep the same semantic grouping across desktop and mobile layouts.
#### Scenario: Desktop layout
- **WHEN** viewport is desktop width
- **THEN** KPI cards SHALL render in multi-column layout
- **THEN** trend and pareto sections SHALL render as two-column analytical panels
#### Scenario: Mobile layout
- **WHEN** viewport width is below responsive breakpoint
- **THEN** cards and chart panels SHALL stack in a single column
- **THEN** filter controls SHALL remain operable without horizontal overflow

View File

@@ -0,0 +1,77 @@
# reject-metric-semantics Specification
## Purpose
TBD - created by archiving change reject-history-query-page. Update Purpose after archive.
## Requirements
### Requirement: Charge-off reject metric SHALL be computed from five reject component columns
The system SHALL compute `REJECT_TOTAL_QTY` as the sum of five reject-related quantity columns.
#### Scenario: Reject total formula
- **WHEN** a source record is transformed
- **THEN** `REJECT_TOTAL_QTY` SHALL equal `REJECTQTY + STANDBYQTY + QTYTOPROCESS + INPROCESSQTY + PROCESSEDQTY`
- **THEN** null component values SHALL be treated as zero
### Requirement: Defect metric SHALL remain independent from reject total
The system SHALL compute `DEFECT_QTY` only from `DEFECTQTY` and SHALL NOT merge it into `REJECT_TOTAL_QTY`.
#### Scenario: Defect independence
- **WHEN** a record has `DEFECTQTY > 0` and reject component sum equals 0
- **THEN** `DEFECT_QTY` SHALL be non-zero
- **THEN** `REJECT_TOTAL_QTY` SHALL remain 0
### Requirement: Yield-exclusion policy SHALL follow ERP exclusion table
The system SHALL use `ERP_PJ_WIP_SCRAP_REASONS_EXCLUDE` as the policy source for "not included in yield" scrap reasons.
#### Scenario: Enabled policy rows
- **WHEN** exclusion policy is evaluated
- **THEN** only rows with `ENABLE_FLAG='Y'` SHALL be considered exclusion rules
#### Scenario: Default exclusion behavior
- **WHEN** `include_excluded_scrap=false` (default)
- **THEN** source rows matching enabled exclusion reasons SHALL be excluded before computing yield-related metrics
#### Scenario: Optional inclusion override
- **WHEN** `include_excluded_scrap=true`
- **THEN** the same matched rows SHALL be included back into metric calculations
### Requirement: Move-in denominator SHALL be deduplicated at event level
The system SHALL deduplicate `MOVEIN_QTY` by event key before rate calculations.
#### Scenario: Primary dedupe key
- **WHEN** `HISTORYMAINLINEID` is present
- **THEN** only one row per `HISTORYMAINLINEID` SHALL contribute `MOVEIN_QTY`
#### Scenario: Fallback dedupe key
- **WHEN** `HISTORYMAINLINEID` is missing
- **THEN** fallback dedupe key SHALL use a deterministic composite key from transaction context
### Requirement: Reject and defect rates SHALL use the same deduplicated denominator
The system SHALL calculate percentage rates from deduplicated `MOVEIN_QTY` to ensure comparability.
#### Scenario: Reject rate formula
- **WHEN** `MOVEIN_QTY > 0`
- **THEN** `REJECT_RATE_PCT` SHALL equal `REJECT_TOTAL_QTY / MOVEIN_QTY * 100`
#### Scenario: Defect rate formula
- **WHEN** `MOVEIN_QTY > 0`
- **THEN** `DEFECT_RATE_PCT` SHALL equal `DEFECT_QTY / MOVEIN_QTY * 100`
#### Scenario: Zero denominator handling
- **WHEN** `MOVEIN_QTY = 0`
- **THEN** both rate fields SHALL return 0 and SHALL NOT raise divide-by-zero errors
### Requirement: Reject share SHALL describe reject proportion within total loss
The system SHALL calculate reject share against combined reject and defect loss quantities.
#### Scenario: Reject share formula
- **WHEN** `REJECT_TOTAL_QTY + DEFECT_QTY > 0`
- **THEN** `REJECT_SHARE_PCT` SHALL equal `REJECT_TOTAL_QTY / (REJECT_TOTAL_QTY + DEFECT_QTY) * 100`
### Requirement: Metric naming SHALL preserve semantic meaning across transformations
The system SHALL keep explicit names for charge-off reject and non-charge-off defect metrics.
#### Scenario: No ambiguous remapping
- **WHEN** service or export fields are generated
- **THEN** `REJECT_TOTAL_QTY` SHALL NOT be renamed to `DEFECT_QTY`
- **THEN** `DEFECT_QTY` SHALL refer only to `DEFECTQTY`

View File

@@ -45,3 +45,27 @@ When contract loading falls back from the primary modernization contract artifac
#### Scenario: Legacy contract fallback path selected
- **WHEN** the primary contract artifact is unavailable and a legacy contract file is loaded
- **THEN** the system SHALL log a warning that includes the selected legacy source path
### Requirement: Reject History route SHALL be included in governed shell route inventory
The `/reject-history` route SHALL be represented in shell route contracts with complete governance metadata.
#### Scenario: Frontend route contract entry
- **WHEN** route contract validation runs against `frontend/src/portal-shell/routeContracts.js`
- **THEN** `/reject-history` SHALL exist with route id, title, owner, render mode, visibility policy, scope, and compatibility policy
#### Scenario: Native loader coverage
- **WHEN** native module loader registry is validated
- **THEN** `/reject-history` SHALL be resolvable in `nativeModuleRegistry`
### Requirement: Reject History governance metadata SHALL be parity-validated across sources
Shell governance checks SHALL enforce parity for `/reject-history` between frontend and backend contract inventories.
#### Scenario: Contract parity for reject-history route
- **WHEN** contract parity checks execute
- **THEN** frontend and backend route inventories SHALL both include `/reject-history`
- **THEN** metadata mismatch or missing route SHALL fail governance checks
#### Scenario: Navigation visibility governance
- **WHEN** page status/navigation config is evaluated
- **THEN** `/reject-history` SHALL have governed drawer assignment and ordering metadata

View File

@@ -137,3 +137,42 @@ The mid-section defect page SHALL use `AbortController` to cancel in-flight API
- **THEN** the query SHALL NOT be cancelled by the pagination request
- **THEN** the pagination SHALL use a separate abort key from the query
### Requirement: Reject History page SHALL be a pure Vite HTML entry
The reject-history page SHALL be built from an HTML entry and emitted as static dist assets.
#### Scenario: Vite entry registration
- **WHEN** Vite config inputs are evaluated
- **THEN** `reject-history` SHALL map to `frontend/src/reject-history/index.html`
#### Scenario: Build output artifacts
- **WHEN** `vite build` completes
- **THEN** output SHALL include `reject-history.html`, `reject-history.js`, and `reject-history.css` in `static/dist/`
### Requirement: Reject History route SHALL serve static dist HTML
The Flask route for `/reject-history` SHALL serve pre-built static HTML through `send_from_directory`.
#### Scenario: Static page serving
- **WHEN** user navigates to `/reject-history`
- **THEN** Flask SHALL serve `static/dist/reject-history.html` when the file exists
- **THEN** HTML SHALL NOT be rendered through Jinja template interpolation
#### Scenario: Dist fallback response
- **WHEN** `reject-history.html` is missing in dist
- **THEN** route SHALL return a minimal fallback HTML that still references `/static/dist/reject-history.js`
### Requirement: Reject History shell integration SHALL use native module loading
The page SHALL integrate with portal-shell native module loading policy.
#### Scenario: Native module registration
- **WHEN** shell resolves a route component for `/reject-history`
- **THEN** it SHALL dynamically import `frontend/src/reject-history/App.vue`
- **THEN** the route style bundle SHALL be loaded via registered style loaders
### Requirement: Reject History page SHALL call APIs through shared core API module
The page SHALL call backend APIs via `frontend/src/core/api.js` without legacy global dependencies.
#### Scenario: API call path
- **WHEN** reject-history page executes GET or export requests
- **THEN** requests SHALL use shared API utilities (`apiGet`/equivalent)
- **THEN** page behavior SHALL NOT depend on `window.MesApi`