feat: 新增 WIP 狀態篩選功能 - 點擊卡片快速篩選 RUN/QUEUE/HOLD
- 後端 API 支援 status 參數篩選 (RUN/QUEUE/HOLD) - WIP Overview: 點擊狀態卡片可篩選 Matrix 顯示 - WIP Detail: 移除舊 Status 下拉選單,改用卡片篩選 - 加入 AbortController 防止快速切換時請求堆積導致 timeout - 選中卡片視覺強化 (放大、深色背景、陰影) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -65,6 +65,7 @@ def api_overview_matrix():
|
||||
workorder: Optional WORKORDER filter (fuzzy match)
|
||||
lotid: Optional LOTID filter (fuzzy match)
|
||||
include_dummy: Include DUMMY lots (default: false)
|
||||
status: Optional WIP status filter ('RUN', 'QUEUE', 'HOLD')
|
||||
|
||||
Returns:
|
||||
JSON with workcenters, packages, matrix, workcenter_totals,
|
||||
@@ -73,11 +74,20 @@ def api_overview_matrix():
|
||||
workorder = request.args.get('workorder', '').strip() or None
|
||||
lotid = request.args.get('lotid', '').strip() or None
|
||||
include_dummy = _parse_bool(request.args.get('include_dummy', ''))
|
||||
status = request.args.get('status', '').strip().upper() or None
|
||||
|
||||
# Validate status parameter
|
||||
if status and status not in ('RUN', 'QUEUE', 'HOLD'):
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': 'Invalid status. Use RUN, QUEUE, or HOLD'
|
||||
}), 400
|
||||
|
||||
result = get_wip_matrix(
|
||||
include_dummy=include_dummy,
|
||||
workorder=workorder,
|
||||
lotid=lotid
|
||||
lotid=lotid,
|
||||
status=status
|
||||
)
|
||||
if result is not None:
|
||||
return jsonify({'success': True, 'data': result})
|
||||
@@ -123,7 +133,7 @@ def api_detail(workcenter: str):
|
||||
|
||||
Query Parameters:
|
||||
package: Optional PRODUCTLINENAME filter
|
||||
status: Optional STATUS filter ('ACTIVE', 'HOLD')
|
||||
status: Optional WIP status filter ('RUN', 'QUEUE', 'HOLD')
|
||||
workorder: Optional WORKORDER filter (fuzzy match)
|
||||
lotid: Optional LOTID filter (fuzzy match)
|
||||
include_dummy: Include DUMMY lots (default: false)
|
||||
@@ -134,7 +144,7 @@ def api_detail(workcenter: str):
|
||||
JSON with workcenter, summary, specs, lots, pagination, sys_date
|
||||
"""
|
||||
package = request.args.get('package', '').strip() or None
|
||||
status = request.args.get('status', '').strip() or None
|
||||
status = request.args.get('status', '').strip().upper() or None
|
||||
workorder = request.args.get('workorder', '').strip() or None
|
||||
lotid = request.args.get('lotid', '').strip() or None
|
||||
include_dummy = _parse_bool(request.args.get('include_dummy', ''))
|
||||
@@ -144,6 +154,13 @@ def api_detail(workcenter: str):
|
||||
if page < 1:
|
||||
page = 1
|
||||
|
||||
# Validate status parameter
|
||||
if status and status not in ('RUN', 'QUEUE', 'HOLD'):
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': 'Invalid status. Use RUN, QUEUE, or HOLD'
|
||||
}), 400
|
||||
|
||||
result = get_wip_detail(
|
||||
workcenter=workcenter,
|
||||
package=package,
|
||||
|
||||
@@ -143,7 +143,8 @@ def get_wip_summary(
|
||||
def get_wip_matrix(
|
||||
include_dummy: bool = False,
|
||||
workorder: Optional[str] = None,
|
||||
lotid: Optional[str] = None
|
||||
lotid: Optional[str] = None,
|
||||
status: Optional[str] = None
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
"""Get workcenter x product line matrix for overview dashboard.
|
||||
|
||||
@@ -151,6 +152,7 @@ def get_wip_matrix(
|
||||
include_dummy: If True, include DUMMY lots (default: False)
|
||||
workorder: Optional WORKORDER filter (fuzzy match)
|
||||
lotid: Optional LOTID filter (fuzzy match)
|
||||
status: Optional WIP status filter ('RUN', 'QUEUE', 'HOLD')
|
||||
|
||||
Returns:
|
||||
Dict with matrix data:
|
||||
@@ -165,6 +167,16 @@ def get_wip_matrix(
|
||||
conditions = _build_base_conditions(include_dummy, workorder, lotid)
|
||||
conditions.append("WORKCENTER_GROUP IS NOT NULL")
|
||||
conditions.append("PRODUCTLINENAME IS NOT NULL")
|
||||
|
||||
# WIP status filter
|
||||
if status:
|
||||
status_upper = status.upper()
|
||||
if status_upper == 'RUN':
|
||||
conditions.append("EQUIPMENTCOUNT > 0")
|
||||
elif status_upper == 'HOLD':
|
||||
conditions.append("EQUIPMENTCOUNT = 0 AND CURRENTHOLDCOUNT > 0")
|
||||
elif status_upper == 'QUEUE':
|
||||
conditions.append("EQUIPMENTCOUNT = 0 AND CURRENTHOLDCOUNT = 0")
|
||||
where_clause = f"WHERE {' AND '.join(conditions)}"
|
||||
|
||||
sql = f"""
|
||||
@@ -303,7 +315,7 @@ def get_wip_detail(
|
||||
Args:
|
||||
workcenter: WORKCENTER_GROUP name
|
||||
package: Optional PRODUCTLINENAME filter
|
||||
status: Optional STATUS filter ('ACTIVE', 'HOLD')
|
||||
status: Optional WIP status filter ('RUN', 'QUEUE', 'HOLD')
|
||||
workorder: Optional WORKORDER filter (fuzzy match)
|
||||
lotid: Optional LOTID filter (fuzzy match)
|
||||
include_dummy: If True, include DUMMY lots (default: False)
|
||||
@@ -313,7 +325,7 @@ def get_wip_detail(
|
||||
Returns:
|
||||
Dict with:
|
||||
- workcenter: The workcenter group name
|
||||
- summary: {total_lots, on_equipment_lots, waiting_lots, hold_lots}
|
||||
- summary: {totalLots, runLots, queueLots, holdLots}
|
||||
- specs: List of spec names (sorted by SPECSEQUENCE)
|
||||
- lots: List of lot details
|
||||
- pagination: {page, page_size, total_count, total_pages}
|
||||
@@ -327,8 +339,15 @@ def get_wip_detail(
|
||||
if package:
|
||||
conditions.append(f"PRODUCTLINENAME = '{_escape_sql(package)}'")
|
||||
|
||||
# WIP status filter (RUN/QUEUE/HOLD based on EQUIPMENTCOUNT and CURRENTHOLDCOUNT)
|
||||
if status:
|
||||
conditions.append(f"STATUS = '{_escape_sql(status)}'")
|
||||
status_upper = status.upper()
|
||||
if status_upper == 'RUN':
|
||||
conditions.append("COALESCE(EQUIPMENTCOUNT, 0) > 0")
|
||||
elif status_upper == 'HOLD':
|
||||
conditions.append("COALESCE(EQUIPMENTCOUNT, 0) = 0 AND COALESCE(CURRENTHOLDCOUNT, 0) > 0")
|
||||
elif status_upper == 'QUEUE':
|
||||
conditions.append("COALESCE(EQUIPMENTCOUNT, 0) = 0 AND COALESCE(CURRENTHOLDCOUNT, 0) = 0")
|
||||
|
||||
where_clause = f"WHERE {' AND '.join(conditions)}"
|
||||
|
||||
|
||||
@@ -328,7 +328,20 @@
|
||||
animation: valueUpdate 0.5s ease;
|
||||
}
|
||||
|
||||
/* Status Card Colors */
|
||||
/* Status Card Colors - Clickable */
|
||||
.summary-card.status-run,
|
||||
.summary-card.status-queue,
|
||||
.summary-card.status-hold {
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.summary-card.status-run:hover,
|
||||
.summary-card.status-queue:hover,
|
||||
.summary-card.status-hold:hover {
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.summary-card.status-run {
|
||||
background: #F0FDF4;
|
||||
border-color: #22C55E;
|
||||
@@ -336,6 +349,15 @@
|
||||
.summary-card.status-run .summary-value {
|
||||
color: #166534;
|
||||
}
|
||||
.summary-card.status-run:hover {
|
||||
box-shadow: 0 4px 15px rgba(34, 197, 94, 0.25);
|
||||
}
|
||||
.summary-card.status-run.active {
|
||||
background: #DCFCE7;
|
||||
border-width: 3px;
|
||||
transform: scale(1.03);
|
||||
box-shadow: 0 6px 25px rgba(34, 197, 94, 0.5);
|
||||
}
|
||||
|
||||
.summary-card.status-queue {
|
||||
background: #FFFBEB;
|
||||
@@ -344,6 +366,15 @@
|
||||
.summary-card.status-queue .summary-value {
|
||||
color: #92400E;
|
||||
}
|
||||
.summary-card.status-queue:hover {
|
||||
box-shadow: 0 4px 15px rgba(245, 158, 11, 0.25);
|
||||
}
|
||||
.summary-card.status-queue.active {
|
||||
background: #FEF3C7;
|
||||
border-width: 3px;
|
||||
transform: scale(1.03);
|
||||
box-shadow: 0 6px 25px rgba(245, 158, 11, 0.5);
|
||||
}
|
||||
|
||||
.summary-card.status-hold {
|
||||
background: #FEF2F2;
|
||||
@@ -352,6 +383,22 @@
|
||||
.summary-card.status-hold .summary-value {
|
||||
color: #991B1B;
|
||||
}
|
||||
.summary-card.status-hold:hover {
|
||||
box-shadow: 0 4px 15px rgba(239, 68, 68, 0.25);
|
||||
}
|
||||
.summary-card.status-hold.active {
|
||||
background: #FEE2E2;
|
||||
border-width: 3px;
|
||||
transform: scale(1.03);
|
||||
box-shadow: 0 6px 25px rgba(239, 68, 68, 0.5);
|
||||
}
|
||||
|
||||
/* Dim non-active cards when filtering */
|
||||
.summary-row.filtering .summary-card.status-run:not(.active),
|
||||
.summary-row.filtering .summary-card.status-queue:not(.active),
|
||||
.summary-row.filtering .summary-card.status-hold:not(.active) {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
@keyframes valueUpdate {
|
||||
0% { transform: scale(1); }
|
||||
@@ -610,33 +657,25 @@
|
||||
<option value="">All</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="filter-group">
|
||||
<label>Status</label>
|
||||
<select id="filterStatus" onchange="applyFilters()">
|
||||
<option value="">All</option>
|
||||
<option value="ACTIVE">Active</option>
|
||||
<option value="HOLD">Hold</option>
|
||||
</select>
|
||||
</div>
|
||||
<button class="btn-primary" onclick="applyFilters()">Apply</button>
|
||||
<button class="btn-secondary" onclick="clearFilters()">Clear</button>
|
||||
</div>
|
||||
|
||||
<!-- Summary Cards -->
|
||||
<div class="summary-row">
|
||||
<div class="summary-row" id="summaryRow">
|
||||
<div class="summary-card">
|
||||
<div class="summary-label">Total Lots</div>
|
||||
<div class="summary-value" id="totalLots">-</div>
|
||||
</div>
|
||||
<div class="summary-card status-run">
|
||||
<div class="summary-card status-run" onclick="toggleStatusFilter('run')" title="Click to filter RUN only">
|
||||
<div class="summary-label">RUN</div>
|
||||
<div class="summary-value" id="runLots">-</div>
|
||||
</div>
|
||||
<div class="summary-card status-queue">
|
||||
<div class="summary-card status-queue" onclick="toggleStatusFilter('queue')" title="Click to filter QUEUE only">
|
||||
<div class="summary-label">QUEUE</div>
|
||||
<div class="summary-value" id="queueLots">-</div>
|
||||
</div>
|
||||
<div class="summary-card status-hold">
|
||||
<div class="summary-card status-hold" onclick="toggleStatusFilter('hold')" title="Click to filter HOLD only">
|
||||
<div class="summary-label">HOLD</div>
|
||||
<div class="summary-value" id="holdLots">-</div>
|
||||
</div>
|
||||
@@ -677,7 +716,6 @@
|
||||
pageSize: 100,
|
||||
filters: {
|
||||
package: '',
|
||||
status: '',
|
||||
workorder: '',
|
||||
lotid: ''
|
||||
},
|
||||
@@ -687,6 +725,12 @@
|
||||
searchDebounceTimers: {}
|
||||
};
|
||||
|
||||
// WIP Status filter (separate from other filters)
|
||||
let activeStatusFilter = null; // null | 'run' | 'queue' | 'hold'
|
||||
|
||||
// AbortController for cancelling in-flight table requests
|
||||
let tableAbortController = null;
|
||||
|
||||
// ============================================================
|
||||
// Utility Functions
|
||||
// ============================================================
|
||||
@@ -717,10 +761,15 @@
|
||||
// ============================================================
|
||||
const API_TIMEOUT = 60000; // 60 seconds timeout
|
||||
|
||||
async function fetchWithTimeout(url, timeout = API_TIMEOUT) {
|
||||
async function fetchWithTimeout(url, timeout = API_TIMEOUT, externalSignal = null) {
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
||||
|
||||
// If external signal is provided, abort when it fires
|
||||
if (externalSignal) {
|
||||
externalSignal.addEventListener('abort', () => controller.abort());
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(url, { signal: controller.signal });
|
||||
clearTimeout(timeoutId);
|
||||
@@ -728,6 +777,10 @@
|
||||
} catch (error) {
|
||||
clearTimeout(timeoutId);
|
||||
if (error.name === 'AbortError') {
|
||||
// Check if it was external cancellation or timeout
|
||||
if (externalSignal && externalSignal.aborted) {
|
||||
throw error; // Re-throw AbortError for external cancellation
|
||||
}
|
||||
throw new Error('Request timeout');
|
||||
}
|
||||
throw error;
|
||||
@@ -743,7 +796,7 @@
|
||||
throw new Error(result.error || 'Failed to fetch packages');
|
||||
}
|
||||
|
||||
async function fetchDetail() {
|
||||
async function fetchDetail(signal = null) {
|
||||
const params = new URLSearchParams({
|
||||
page: state.page,
|
||||
page_size: state.pageSize
|
||||
@@ -752,8 +805,9 @@
|
||||
if (state.filters.package) {
|
||||
params.append('package', state.filters.package);
|
||||
}
|
||||
if (state.filters.status) {
|
||||
params.append('status', state.filters.status);
|
||||
if (activeStatusFilter) {
|
||||
// Convert to API status format (RUN/QUEUE/HOLD)
|
||||
params.append('status', activeStatusFilter.toUpperCase());
|
||||
}
|
||||
if (state.filters.workorder) {
|
||||
params.append('workorder', state.filters.workorder);
|
||||
@@ -762,7 +816,7 @@
|
||||
params.append('lotid', state.filters.lotid);
|
||||
}
|
||||
|
||||
const response = await fetchWithTimeout(`/api/wip/detail/${encodeURIComponent(state.workcenter)}?${params}`);
|
||||
const response = await fetchWithTimeout(`/api/wip/detail/${encodeURIComponent(state.workcenter)}?${params}`, API_TIMEOUT, signal);
|
||||
const result = await response.json();
|
||||
if (result.success) {
|
||||
return result.data;
|
||||
@@ -1033,6 +1087,103 @@
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Status Filter Toggle (Clickable Cards)
|
||||
// ============================================================
|
||||
function toggleStatusFilter(status) {
|
||||
if (activeStatusFilter === status) {
|
||||
// Clicking the same card again removes the filter
|
||||
activeStatusFilter = null;
|
||||
} else {
|
||||
// Apply new filter
|
||||
activeStatusFilter = status;
|
||||
}
|
||||
|
||||
// Update card styles
|
||||
updateCardStyles();
|
||||
|
||||
// Update table title
|
||||
updateTableTitle();
|
||||
|
||||
// Reset to page 1 and reload table only (no isLoading guard)
|
||||
state.page = 1;
|
||||
loadTableOnly();
|
||||
}
|
||||
|
||||
async function loadTableOnly() {
|
||||
// Cancel any in-flight request to prevent pile-up
|
||||
if (tableAbortController) {
|
||||
tableAbortController.abort();
|
||||
}
|
||||
tableAbortController = new AbortController();
|
||||
|
||||
// Show loading in table container
|
||||
const container = document.getElementById('tableContainer');
|
||||
container.innerHTML = '<div class="placeholder">Loading...</div>';
|
||||
|
||||
// Show refresh indicator
|
||||
document.getElementById('refreshIndicator').classList.add('active');
|
||||
|
||||
try {
|
||||
state.data = await fetchDetail(tableAbortController.signal);
|
||||
renderSummary(state.data.summary);
|
||||
renderTable(state.data);
|
||||
|
||||
// Show success indicator
|
||||
document.getElementById('refreshSuccess').classList.add('active');
|
||||
setTimeout(() => {
|
||||
document.getElementById('refreshSuccess').classList.remove('active');
|
||||
}, 1500);
|
||||
} catch (error) {
|
||||
// Ignore abort errors (expected when user clicks quickly)
|
||||
if (error.name === 'AbortError') {
|
||||
console.log('[WIP Detail] Table request cancelled (new filter selected)');
|
||||
return;
|
||||
}
|
||||
console.error('Table load failed:', error);
|
||||
container.innerHTML = '<div class="placeholder">Error loading data</div>';
|
||||
document.getElementById('refreshError').classList.add('active');
|
||||
} finally {
|
||||
document.getElementById('refreshIndicator').classList.remove('active');
|
||||
}
|
||||
}
|
||||
|
||||
function updateCardStyles() {
|
||||
const row = document.getElementById('summaryRow');
|
||||
const statusCards = document.querySelectorAll('.summary-card.status-run, .summary-card.status-queue, .summary-card.status-hold');
|
||||
|
||||
// Remove active from all status cards
|
||||
statusCards.forEach(card => {
|
||||
card.classList.remove('active');
|
||||
});
|
||||
|
||||
if (activeStatusFilter) {
|
||||
// Add filtering class to row (dims non-active cards)
|
||||
row.classList.add('filtering');
|
||||
|
||||
// Add active to the selected card
|
||||
const activeCard = document.querySelector(`.summary-card.status-${activeStatusFilter}`);
|
||||
if (activeCard) {
|
||||
activeCard.classList.add('active');
|
||||
}
|
||||
} else {
|
||||
// Remove filtering class
|
||||
row.classList.remove('filtering');
|
||||
}
|
||||
}
|
||||
|
||||
function updateTableTitle() {
|
||||
const titleEl = document.querySelector('.table-title');
|
||||
const baseTitle = 'Lot Details';
|
||||
|
||||
if (activeStatusFilter) {
|
||||
const statusLabel = activeStatusFilter.toUpperCase();
|
||||
titleEl.textContent = `${baseTitle} - ${statusLabel} Only`;
|
||||
} else {
|
||||
titleEl.textContent = baseTitle;
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Filter & Pagination
|
||||
// ============================================================
|
||||
@@ -1040,7 +1191,6 @@
|
||||
state.filters.workorder = document.getElementById('filterWorkorder').value.trim();
|
||||
state.filters.lotid = document.getElementById('filterLotid').value.trim();
|
||||
state.filters.package = document.getElementById('filterPackage').value;
|
||||
state.filters.status = document.getElementById('filterStatus').value;
|
||||
state.page = 1;
|
||||
loadAllData(false);
|
||||
}
|
||||
@@ -1049,8 +1199,13 @@
|
||||
document.getElementById('filterWorkorder').value = '';
|
||||
document.getElementById('filterLotid').value = '';
|
||||
document.getElementById('filterPackage').value = '';
|
||||
document.getElementById('filterStatus').value = '';
|
||||
state.filters = { package: '', status: '', workorder: '', lotid: '' };
|
||||
state.filters = { package: '', workorder: '', lotid: '' };
|
||||
|
||||
// Also clear status filter
|
||||
activeStatusFilter = null;
|
||||
updateCardStyles();
|
||||
updateTableTitle();
|
||||
|
||||
state.page = 1;
|
||||
loadAllData(false);
|
||||
}
|
||||
|
||||
@@ -388,6 +388,42 @@
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
/* Clickable status cards */
|
||||
.wip-status-card {
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.wip-status-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.wip-status-card.active {
|
||||
border-width: 4px;
|
||||
transform: scale(1.03);
|
||||
}
|
||||
|
||||
.wip-status-card.run.active {
|
||||
background: #DCFCE7;
|
||||
box-shadow: 0 6px 25px rgba(34, 197, 94, 0.5);
|
||||
}
|
||||
|
||||
.wip-status-card.queue.active {
|
||||
background: #FEF3C7;
|
||||
box-shadow: 0 6px 25px rgba(245, 158, 11, 0.5);
|
||||
}
|
||||
|
||||
.wip-status-card.hold.active {
|
||||
background: #FEE2E2;
|
||||
box-shadow: 0 6px 25px rgba(239, 68, 68, 0.5);
|
||||
}
|
||||
|
||||
/* Dim non-active cards when filtering */
|
||||
.wip-status-row.filtering .wip-status-card:not(.active) {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
/* Content Grid */
|
||||
.content-grid {
|
||||
display: grid;
|
||||
@@ -647,21 +683,21 @@
|
||||
|
||||
<!-- WIP Status Cards -->
|
||||
<div class="wip-status-row">
|
||||
<div class="wip-status-card run">
|
||||
<div class="wip-status-card run" onclick="toggleStatusFilter('run')">
|
||||
<div class="status-header"><span class="dot"></span>RUN</div>
|
||||
<div class="status-values">
|
||||
<span id="runLots">-</span>
|
||||
<span id="runQty">-</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="wip-status-card queue">
|
||||
<div class="wip-status-card queue" onclick="toggleStatusFilter('queue')">
|
||||
<div class="status-header"><span class="dot"></span>QUEUE</div>
|
||||
<div class="status-values">
|
||||
<span id="queueLots">-</span>
|
||||
<span id="queueQty">-</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="wip-status-card hold">
|
||||
<div class="wip-status-card hold" onclick="toggleStatusFilter('hold')">
|
||||
<div class="status-header"><span class="dot"></span>HOLD</div>
|
||||
<div class="status-values">
|
||||
<span id="holdLots">-</span>
|
||||
@@ -726,6 +762,12 @@
|
||||
}
|
||||
};
|
||||
|
||||
// Status filter state (null = no filter, 'run'/'queue'/'hold' = filtered)
|
||||
let activeStatusFilter = null;
|
||||
|
||||
// AbortController for cancelling in-flight matrix requests
|
||||
let matrixAbortController = null;
|
||||
|
||||
// ============================================================
|
||||
// Utility Functions
|
||||
// ============================================================
|
||||
@@ -906,10 +948,15 @@
|
||||
// ============================================================
|
||||
const API_TIMEOUT = 60000; // 60 seconds timeout
|
||||
|
||||
async function fetchWithTimeout(url, timeout = API_TIMEOUT) {
|
||||
async function fetchWithTimeout(url, timeout = API_TIMEOUT, externalSignal = null) {
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
||||
|
||||
// If external signal is provided, abort when it fires
|
||||
if (externalSignal) {
|
||||
externalSignal.addEventListener('abort', () => controller.abort());
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(url, { signal: controller.signal });
|
||||
clearTimeout(timeoutId);
|
||||
@@ -917,6 +964,10 @@
|
||||
} catch (error) {
|
||||
clearTimeout(timeoutId);
|
||||
if (error.name === 'AbortError') {
|
||||
// Check if it was external cancellation or timeout
|
||||
if (externalSignal && externalSignal.aborted) {
|
||||
throw error; // Re-throw AbortError for external cancellation
|
||||
}
|
||||
throw new Error('Request timeout');
|
||||
}
|
||||
throw error;
|
||||
@@ -934,10 +985,21 @@
|
||||
throw new Error(result.error || 'Failed to fetch summary');
|
||||
}
|
||||
|
||||
async function fetchMatrix() {
|
||||
const queryParams = buildQueryParams();
|
||||
async function fetchMatrix(signal = null) {
|
||||
const params = new URLSearchParams();
|
||||
if (state.filters.workorder) {
|
||||
params.append('workorder', state.filters.workorder);
|
||||
}
|
||||
if (state.filters.lotid) {
|
||||
params.append('lotid', state.filters.lotid);
|
||||
}
|
||||
// Add status filter if active
|
||||
if (activeStatusFilter) {
|
||||
params.append('status', activeStatusFilter.toUpperCase());
|
||||
}
|
||||
const queryParams = params.toString();
|
||||
const url = `/api/wip/overview/matrix${queryParams ? '?' + queryParams : ''}`;
|
||||
const response = await fetchWithTimeout(url);
|
||||
const response = await fetchWithTimeout(url, API_TIMEOUT, signal);
|
||||
const result = await response.json();
|
||||
if (result.success) {
|
||||
return result.data;
|
||||
@@ -1003,6 +1065,78 @@
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Status Filter Functions
|
||||
// ============================================================
|
||||
function toggleStatusFilter(status) {
|
||||
if (activeStatusFilter === status) {
|
||||
// Deactivate filter
|
||||
activeStatusFilter = null;
|
||||
} else {
|
||||
// Activate new filter
|
||||
activeStatusFilter = status;
|
||||
}
|
||||
|
||||
updateCardStyles();
|
||||
updateMatrixTitle();
|
||||
loadMatrixOnly();
|
||||
}
|
||||
|
||||
function updateCardStyles() {
|
||||
const row = document.querySelector('.wip-status-row');
|
||||
document.querySelectorAll('.wip-status-card').forEach(card => {
|
||||
card.classList.remove('active');
|
||||
});
|
||||
|
||||
if (activeStatusFilter) {
|
||||
row.classList.add('filtering');
|
||||
const activeCard = document.querySelector(`.wip-status-card.${activeStatusFilter}`);
|
||||
if (activeCard) {
|
||||
activeCard.classList.add('active');
|
||||
}
|
||||
} else {
|
||||
row.classList.remove('filtering');
|
||||
}
|
||||
}
|
||||
|
||||
function updateMatrixTitle() {
|
||||
const titleEl = document.querySelector('.card-title');
|
||||
if (!titleEl) return;
|
||||
|
||||
const baseTitle = 'Workcenter x Package Matrix (QTY)';
|
||||
if (activeStatusFilter) {
|
||||
const statusLabel = activeStatusFilter.toUpperCase();
|
||||
titleEl.textContent = `${baseTitle} - ${statusLabel} Only`;
|
||||
} else {
|
||||
titleEl.textContent = baseTitle;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadMatrixOnly() {
|
||||
// Cancel any in-flight matrix request to prevent pile-up
|
||||
if (matrixAbortController) {
|
||||
matrixAbortController.abort();
|
||||
}
|
||||
matrixAbortController = new AbortController();
|
||||
|
||||
const container = document.getElementById('matrixContainer');
|
||||
container.innerHTML = '<div class="placeholder">Loading...</div>';
|
||||
|
||||
try {
|
||||
const matrix = await fetchMatrix(matrixAbortController.signal);
|
||||
state.matrix = matrix;
|
||||
renderMatrix(matrix);
|
||||
} catch (error) {
|
||||
// Ignore abort errors (expected when user clicks quickly)
|
||||
if (error.name === 'AbortError') {
|
||||
console.log('[WIP Overview] Matrix request cancelled (new filter selected)');
|
||||
return;
|
||||
}
|
||||
console.error('[WIP Overview] Matrix load failed:', error);
|
||||
container.innerHTML = '<div class="placeholder">Error loading data</div>';
|
||||
}
|
||||
}
|
||||
|
||||
function renderMatrix(data) {
|
||||
const container = document.getElementById('matrixContainer');
|
||||
|
||||
|
||||
Reference in New Issue
Block a user