feat: WIP Detail 頁面新增 Lot 詳細資訊面板
- 點選 Lot Details 表格中的 LOT ID 可展開詳細資訊面板 - 新增 /api/wip/lot/<lotid> API 端點 - 詳細資訊欄位名稱採用 PowerBI 命名慣例 - 修正 numpy 型別 JSON 序列化問題 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -18,6 +18,7 @@ from mes_dashboard.services.wip_service import (
|
||||
search_lot_ids,
|
||||
search_packages,
|
||||
search_types,
|
||||
get_lot_detail,
|
||||
)
|
||||
|
||||
# Create Blueprint
|
||||
@@ -213,6 +214,23 @@ def api_detail(workcenter: str):
|
||||
return jsonify({'success': False, 'error': '查詢失敗'}), 500
|
||||
|
||||
|
||||
@wip_bp.route('/lot/<lotid>')
|
||||
def api_lot_detail(lotid: str):
|
||||
"""API: Get detailed information for a specific lot.
|
||||
|
||||
Args:
|
||||
lotid: LOTID (URL path parameter)
|
||||
|
||||
Returns:
|
||||
JSON with lot details including all fields from DW_MES_LOT_V
|
||||
"""
|
||||
result = get_lot_detail(lotid)
|
||||
|
||||
if result is not None:
|
||||
return jsonify({'success': True, 'data': result})
|
||||
return jsonify({'success': False, 'error': '找不到此批號'}), 404
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Meta APIs
|
||||
# ============================================================
|
||||
|
||||
@@ -20,9 +20,12 @@ logger = logging.getLogger('mes_dashboard.wip_service')
|
||||
|
||||
|
||||
def _safe_value(val):
|
||||
"""Convert pandas NaN/NaT to None for JSON serialization."""
|
||||
"""Convert pandas NaN/NaT to None and numpy types to native Python types for JSON serialization."""
|
||||
if pd.isna(val):
|
||||
return None
|
||||
# Convert numpy types to native Python types for JSON serialization
|
||||
if hasattr(val, 'item'): # numpy scalar types have .item() method
|
||||
return val.item()
|
||||
return val
|
||||
|
||||
|
||||
@@ -2145,3 +2148,276 @@ def _get_hold_detail_lots_from_oracle(
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return None
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Lot Detail API Functions
|
||||
# ============================================================
|
||||
|
||||
# Field labels mapping for lot detail display (PowerBI naming convention)
|
||||
LOT_DETAIL_FIELD_LABELS = {
|
||||
'lotId': 'Run Card Lot ID',
|
||||
'workorder': 'Work Order ID',
|
||||
'qty': 'Lot Qty(pcs)',
|
||||
'qty2': 'Lot Qty(Wafer pcs)',
|
||||
'status': 'Run Card Status',
|
||||
'holdReason': 'Hold Reason',
|
||||
'holdCount': 'Hold Count',
|
||||
'owner': 'Work Order Owner',
|
||||
'startDate': 'Run Card Start Date',
|
||||
'uts': 'UTS',
|
||||
'product': 'Product P/N',
|
||||
'productLine': 'Package',
|
||||
'packageLef': 'Package(LF)',
|
||||
'pjFunction': 'Product Function',
|
||||
'pjType': 'Product Type',
|
||||
'bop': 'BOP',
|
||||
'waferLotId': 'Wafer Lot ID',
|
||||
'waferPn': 'Wafer P/N',
|
||||
'waferLotPrefix': 'Wafer Lot ID(Prefix)',
|
||||
'spec': 'Spec',
|
||||
'specSequence': 'Spec Sequence',
|
||||
'workcenter': 'Work Center',
|
||||
'workcenterSequence': 'Work Center Sequence',
|
||||
'workcenterGroup': 'Work Center(Group)',
|
||||
'workcenterShort': 'Work Center(Short)',
|
||||
'ageByDays': 'Age By Days',
|
||||
'equipment': 'Equipment ID',
|
||||
'equipmentCount': 'Equipment Count',
|
||||
'workflow': 'Work Flow Name',
|
||||
'dateCode': 'Product Date Code',
|
||||
'leadframeName': 'LF Material Part',
|
||||
'leadframeOption': 'LF Option ID',
|
||||
'compoundName': 'Compound Material Part',
|
||||
'location': 'Run Card Location',
|
||||
'ncrId': 'NCR ID',
|
||||
'ncrDate': 'NCR-issued Time',
|
||||
'releaseTime': 'Release Time',
|
||||
'releaseEmp': 'Release Employee',
|
||||
'releaseComment': 'Release Comment',
|
||||
'holdComment': 'Hold Comment',
|
||||
'comment': 'Comment',
|
||||
'commentDate': 'Run Card Comment',
|
||||
'commentEmp': 'Run Card Comment Employee',
|
||||
'futureHoldComment': 'Future Hold Comment',
|
||||
'holdEmp': 'Hold Employee',
|
||||
'holdDept': 'Hold Employee Dept',
|
||||
'produceRegion': 'Produce Region',
|
||||
'priority': 'Work Order Priority',
|
||||
'tmttRemaining': 'TMTT Remaining',
|
||||
'dieConsumption': 'Die Consumption Qty',
|
||||
'wipStatus': 'WIP Status',
|
||||
'dataUpdateDate': 'Data Update Date'
|
||||
}
|
||||
|
||||
|
||||
def get_lot_detail(lotid: str) -> Optional[Dict[str, Any]]:
|
||||
"""Get detailed information for a specific lot.
|
||||
|
||||
Uses Redis cache when available, falls back to Oracle direct query.
|
||||
|
||||
Args:
|
||||
lotid: The LOTID to retrieve
|
||||
|
||||
Returns:
|
||||
Dict with lot details or None if not found
|
||||
"""
|
||||
# Try cache first
|
||||
cached_df = _get_wip_dataframe()
|
||||
if cached_df is not None:
|
||||
try:
|
||||
df = cached_df[cached_df['LOTID'] == lotid]
|
||||
|
||||
if df.empty:
|
||||
return None
|
||||
|
||||
row = df.iloc[0]
|
||||
return _build_lot_detail_response(row)
|
||||
except Exception as exc:
|
||||
logger.warning(f"Cache-based lot detail failed, falling back to Oracle: {exc}")
|
||||
|
||||
# Fallback to Oracle direct query
|
||||
return _get_lot_detail_from_oracle(lotid)
|
||||
|
||||
|
||||
def _get_lot_detail_from_oracle(lotid: str) -> Optional[Dict[str, Any]]:
|
||||
"""Get lot detail directly from Oracle (fallback)."""
|
||||
try:
|
||||
sql = f"""
|
||||
SELECT
|
||||
LOTID,
|
||||
WORKORDER,
|
||||
QTY,
|
||||
QTY2,
|
||||
STATUS,
|
||||
HOLDREASONNAME,
|
||||
CURRENTHOLDCOUNT,
|
||||
OWNER,
|
||||
STARTDATE,
|
||||
UTS,
|
||||
PRODUCT,
|
||||
PRODUCTLINENAME,
|
||||
PACKAGE_LEF,
|
||||
PJ_FUNCTION,
|
||||
PJ_TYPE,
|
||||
BOP,
|
||||
FIRSTNAME,
|
||||
WAFERNAME,
|
||||
WAFERLOT,
|
||||
SPECNAME,
|
||||
SPECSEQUENCE,
|
||||
WORKCENTERNAME,
|
||||
WORKCENTERSEQUENCE,
|
||||
WORKCENTER_GROUP,
|
||||
WORKCENTER_SHORT,
|
||||
AGEBYDAYS,
|
||||
EQUIPMENTS,
|
||||
EQUIPMENTCOUNT,
|
||||
WORKFLOWNAME,
|
||||
DATECODE,
|
||||
LEADFRAMENAME,
|
||||
LEADFRAMEOPTION,
|
||||
COMNAME,
|
||||
LOCATIONNAME,
|
||||
EVENTNAME,
|
||||
OCCURRENCEDATE,
|
||||
RELEASETIME,
|
||||
RELEASEEMP,
|
||||
RELEASEREASON,
|
||||
COMMENT_HOLD,
|
||||
CONTAINERCOMMENTS,
|
||||
COMMENT_DATE,
|
||||
COMMENT_EMP,
|
||||
COMMENT_FUTURE,
|
||||
HOLDEMP,
|
||||
DEPTNAME,
|
||||
PJ_PRODUCEREGION,
|
||||
PRIORITYCODENAME,
|
||||
TMTT_R,
|
||||
WAFER_FACTOR,
|
||||
SYS_DATE
|
||||
FROM {WIP_VIEW}
|
||||
WHERE LOTID = '{_escape_sql(lotid)}'
|
||||
"""
|
||||
df = read_sql_df(sql)
|
||||
|
||||
if df is None or df.empty:
|
||||
return None
|
||||
|
||||
row = df.iloc[0]
|
||||
return _build_lot_detail_response(row)
|
||||
except Exception as exc:
|
||||
logger.error(f"Lot detail query failed: {exc}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return None
|
||||
|
||||
|
||||
def _build_lot_detail_response(row) -> Dict[str, Any]:
|
||||
"""Build lot detail response from DataFrame row."""
|
||||
# Helper to safely get value from row (handles NaN and missing columns)
|
||||
def safe_get(col, default=None):
|
||||
try:
|
||||
val = row.get(col)
|
||||
if pd.isna(val):
|
||||
return default
|
||||
return val
|
||||
except Exception:
|
||||
return default
|
||||
|
||||
# Helper to safely get int value
|
||||
def safe_int(col, default=0):
|
||||
val = safe_get(col)
|
||||
if val is None:
|
||||
return default
|
||||
try:
|
||||
return int(val)
|
||||
except (ValueError, TypeError):
|
||||
return default
|
||||
|
||||
# Helper to safely get float value
|
||||
def safe_float(col, default=0.0):
|
||||
val = safe_get(col)
|
||||
if val is None:
|
||||
return default
|
||||
try:
|
||||
return float(val)
|
||||
except (ValueError, TypeError):
|
||||
return default
|
||||
|
||||
# Helper to format date value
|
||||
def format_date(col):
|
||||
val = safe_get(col)
|
||||
if val is None:
|
||||
return None
|
||||
try:
|
||||
return str(val)
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
# Compute WIP status
|
||||
equipment_count = safe_int('EQUIPMENTCOUNT')
|
||||
hold_count = safe_int('CURRENTHOLDCOUNT')
|
||||
|
||||
if equipment_count > 0:
|
||||
wip_status = 'RUN'
|
||||
elif hold_count > 0:
|
||||
wip_status = 'HOLD'
|
||||
else:
|
||||
wip_status = 'QUEUE'
|
||||
|
||||
return {
|
||||
'lotId': _safe_value(safe_get('LOTID')),
|
||||
'workorder': _safe_value(safe_get('WORKORDER')),
|
||||
'qty': safe_int('QTY'),
|
||||
'qty2': safe_int('QTY2') if safe_get('QTY2') is not None else None,
|
||||
'status': _safe_value(safe_get('STATUS')),
|
||||
'holdReason': _safe_value(safe_get('HOLDREASONNAME')),
|
||||
'holdCount': hold_count,
|
||||
'owner': _safe_value(safe_get('OWNER')),
|
||||
'startDate': format_date('STARTDATE'),
|
||||
'uts': _safe_value(safe_get('UTS')),
|
||||
'product': _safe_value(safe_get('PRODUCT')),
|
||||
'productLine': _safe_value(safe_get('PRODUCTLINENAME')),
|
||||
'packageLef': _safe_value(safe_get('PACKAGE_LEF')),
|
||||
'pjFunction': _safe_value(safe_get('PJ_FUNCTION')),
|
||||
'pjType': _safe_value(safe_get('PJ_TYPE')),
|
||||
'bop': _safe_value(safe_get('BOP')),
|
||||
'waferLotId': _safe_value(safe_get('FIRSTNAME')),
|
||||
'waferPn': _safe_value(safe_get('WAFERNAME')),
|
||||
'waferLotPrefix': _safe_value(safe_get('WAFERLOT')),
|
||||
'spec': _safe_value(safe_get('SPECNAME')),
|
||||
'specSequence': safe_int('SPECSEQUENCE') if safe_get('SPECSEQUENCE') is not None else None,
|
||||
'workcenter': _safe_value(safe_get('WORKCENTERNAME')),
|
||||
'workcenterSequence': safe_int('WORKCENTERSEQUENCE') if safe_get('WORKCENTERSEQUENCE') is not None else None,
|
||||
'workcenterGroup': _safe_value(safe_get('WORKCENTER_GROUP')),
|
||||
'workcenterShort': _safe_value(safe_get('WORKCENTER_SHORT')),
|
||||
'ageByDays': round(safe_float('AGEBYDAYS'), 2),
|
||||
'equipment': _safe_value(safe_get('EQUIPMENTS')),
|
||||
'equipmentCount': equipment_count,
|
||||
'workflow': _safe_value(safe_get('WORKFLOWNAME')),
|
||||
'dateCode': _safe_value(safe_get('DATECODE')),
|
||||
'leadframeName': _safe_value(safe_get('LEADFRAMENAME')),
|
||||
'leadframeOption': _safe_value(safe_get('LEADFRAMEOPTION')),
|
||||
'compoundName': _safe_value(safe_get('COMNAME')),
|
||||
'location': _safe_value(safe_get('LOCATIONNAME')),
|
||||
'ncrId': _safe_value(safe_get('EVENTNAME')),
|
||||
'ncrDate': format_date('OCCURRENCEDATE'),
|
||||
'releaseTime': format_date('RELEASETIME'),
|
||||
'releaseEmp': _safe_value(safe_get('RELEASEEMP')),
|
||||
'releaseComment': _safe_value(safe_get('RELEASEREASON')),
|
||||
'holdComment': _safe_value(safe_get('COMMENT_HOLD')),
|
||||
'comment': _safe_value(safe_get('CONTAINERCOMMENTS')),
|
||||
'commentDate': _safe_value(safe_get('COMMENT_DATE')),
|
||||
'commentEmp': _safe_value(safe_get('COMMENT_EMP')),
|
||||
'futureHoldComment': _safe_value(safe_get('COMMENT_FUTURE')),
|
||||
'holdEmp': _safe_value(safe_get('HOLDEMP')),
|
||||
'holdDept': _safe_value(safe_get('DEPTNAME')),
|
||||
'produceRegion': _safe_value(safe_get('PJ_PRODUCEREGION')),
|
||||
'priority': _safe_value(safe_get('PRIORITYCODENAME')),
|
||||
'tmttRemaining': _safe_value(safe_get('TMTT_R')),
|
||||
'dieConsumption': safe_int('WAFER_FACTOR') if safe_get('WAFER_FACTOR') is not None else None,
|
||||
'wipStatus': wip_status,
|
||||
'dataUpdateDate': format_date('SYS_DATE'),
|
||||
'fieldLabels': LOT_DETAIL_FIELD_LABELS
|
||||
}
|
||||
|
||||
@@ -614,11 +614,163 @@
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
/* Lot Detail Panel */
|
||||
.lot-detail-panel {
|
||||
background: var(--card-bg);
|
||||
border-radius: 10px;
|
||||
box-shadow: var(--shadow);
|
||||
margin-top: 16px;
|
||||
overflow: hidden;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.lot-detail-panel.show {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.lot-detail-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 14px 20px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
}
|
||||
|
||||
.lot-detail-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.lot-detail-title .lot-id {
|
||||
background: rgba(255,255,255,0.2);
|
||||
padding: 4px 12px;
|
||||
border-radius: 6px;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.lot-detail-close {
|
||||
background: rgba(255,255,255,0.2);
|
||||
border: none;
|
||||
color: white;
|
||||
padding: 6px 12px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.lot-detail-close:hover {
|
||||
background: rgba(255,255,255,0.3);
|
||||
}
|
||||
|
||||
.lot-detail-content {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.lot-detail-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.lot-detail-section {
|
||||
background: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
padding: 14px;
|
||||
}
|
||||
|
||||
.lot-detail-section-title {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--primary);
|
||||
margin-bottom: 12px;
|
||||
padding-bottom: 6px;
|
||||
border-bottom: 2px solid var(--primary);
|
||||
}
|
||||
|
||||
.lot-detail-field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.lot-detail-field:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.lot-detail-label {
|
||||
font-size: 11px;
|
||||
color: var(--muted);
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.lot-detail-value {
|
||||
font-size: 13px;
|
||||
color: var(--text);
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.lot-detail-value.empty {
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
.lot-detail-value.status-run {
|
||||
color: #166534;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.lot-detail-value.status-queue {
|
||||
color: #92400E;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.lot-detail-value.status-hold {
|
||||
color: #991B1B;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.lot-detail-loading {
|
||||
text-align: center;
|
||||
padding: 40px;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.lot-detail-loading .loading-spinner {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
/* Clickable LOT ID in table */
|
||||
.lot-id-link {
|
||||
color: var(--primary);
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
.lot-id-link:hover {
|
||||
color: var(--primary-dark);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.lot-id-link.active {
|
||||
background: rgba(102, 126, 234, 0.15);
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 1400px) {
|
||||
.summary-row {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
}
|
||||
.lot-detail-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1000px) {
|
||||
@@ -638,6 +790,9 @@
|
||||
.summary-row {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
.lot-detail-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
@@ -734,6 +889,21 @@
|
||||
<button id="btnNext" onclick="nextPage()">Next</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Lot Detail Panel -->
|
||||
<div class="lot-detail-panel" id="lotDetailPanel">
|
||||
<div class="lot-detail-header">
|
||||
<div class="lot-detail-title">
|
||||
Lot Detail - <span class="lot-id" id="lotDetailLotId"></span>
|
||||
</div>
|
||||
<button class="lot-detail-close" onclick="closeLotDetail()">Close</button>
|
||||
</div>
|
||||
<div class="lot-detail-content" id="lotDetailContent">
|
||||
<div class="lot-detail-loading">
|
||||
<span class="loading-spinner"></span>Loading...
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Loading Overlay -->
|
||||
@@ -956,8 +1126,11 @@
|
||||
data.lots.forEach(lot => {
|
||||
html += '<tr>';
|
||||
|
||||
// Fixed columns
|
||||
html += `<td class="fixed-col">${lot.lotId || '-'}</td>`;
|
||||
// Fixed columns - LOT ID is clickable
|
||||
const lotIdDisplay = lot.lotId
|
||||
? `<span class="lot-id-link" onclick="showLotDetail('${lot.lotId}')">${lot.lotId}</span>`
|
||||
: '-';
|
||||
html += `<td class="fixed-col">${lotIdDisplay}</td>`;
|
||||
html += `<td class="fixed-col">${lot.equipment || '<span style="color: var(--muted);">-</span>'}</td>`;
|
||||
|
||||
// WIP Status with color and hold reason
|
||||
@@ -1329,6 +1502,206 @@
|
||||
loadAllData(false);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Lot Detail Functions
|
||||
// ============================================================
|
||||
let selectedLotId = null;
|
||||
|
||||
async function fetchLotDetail(lotId) {
|
||||
const result = await MesApi.get(`/api/wip/lot/${encodeURIComponent(lotId)}`, {
|
||||
timeout: API_TIMEOUT
|
||||
});
|
||||
if (result.success) {
|
||||
return result.data;
|
||||
}
|
||||
throw new Error(result.error || 'Failed to fetch lot detail');
|
||||
}
|
||||
|
||||
async function showLotDetail(lotId) {
|
||||
// Update selected state
|
||||
selectedLotId = lotId;
|
||||
|
||||
// Highlight the selected row
|
||||
document.querySelectorAll('.lot-id-link').forEach(el => {
|
||||
el.classList.toggle('active', el.textContent === lotId);
|
||||
});
|
||||
|
||||
// Show panel
|
||||
const panel = document.getElementById('lotDetailPanel');
|
||||
panel.classList.add('show');
|
||||
|
||||
// Update title
|
||||
document.getElementById('lotDetailLotId').textContent = lotId;
|
||||
|
||||
// Show loading
|
||||
document.getElementById('lotDetailContent').innerHTML = `
|
||||
<div class="lot-detail-loading">
|
||||
<span class="loading-spinner"></span>Loading...
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Scroll to panel
|
||||
panel.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
|
||||
try {
|
||||
const data = await fetchLotDetail(lotId);
|
||||
renderLotDetail(data);
|
||||
} catch (error) {
|
||||
console.error('Failed to load lot detail:', error);
|
||||
document.getElementById('lotDetailContent').innerHTML = `
|
||||
<div class="lot-detail-loading" style="color: var(--danger);">
|
||||
載入失敗:${error.message || '未知錯誤'}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
function renderLotDetail(data) {
|
||||
const labels = data.fieldLabels || {};
|
||||
|
||||
// Helper to format value
|
||||
const formatValue = (value) => {
|
||||
if (value === null || value === undefined || value === '') {
|
||||
return '<span class="empty">-</span>';
|
||||
}
|
||||
if (typeof value === 'number') {
|
||||
return formatNumber(value);
|
||||
}
|
||||
return value;
|
||||
};
|
||||
|
||||
// Helper to create field HTML
|
||||
const field = (key, customLabel = null) => {
|
||||
const label = customLabel || labels[key] || key;
|
||||
const value = data[key];
|
||||
let valueClass = '';
|
||||
|
||||
// Special styling for WIP Status
|
||||
if (key === 'wipStatus') {
|
||||
valueClass = `status-${(value || '').toLowerCase()}`;
|
||||
}
|
||||
|
||||
return `
|
||||
<div class="lot-detail-field">
|
||||
<span class="lot-detail-label">${label}</span>
|
||||
<span class="lot-detail-value ${valueClass}">${formatValue(value)}</span>
|
||||
</div>
|
||||
`;
|
||||
};
|
||||
|
||||
const html = `
|
||||
<div class="lot-detail-grid">
|
||||
<!-- Basic Info -->
|
||||
<div class="lot-detail-section">
|
||||
<div class="lot-detail-section-title">基本資訊</div>
|
||||
${field('lotId')}
|
||||
${field('workorder')}
|
||||
${field('wipStatus')}
|
||||
${field('status')}
|
||||
${field('qty')}
|
||||
${field('qty2')}
|
||||
${field('ageByDays')}
|
||||
${field('priority')}
|
||||
</div>
|
||||
|
||||
<!-- Product Info -->
|
||||
<div class="lot-detail-section">
|
||||
<div class="lot-detail-section-title">產品資訊</div>
|
||||
${field('product')}
|
||||
${field('productLine')}
|
||||
${field('packageLef')}
|
||||
${field('pjType')}
|
||||
${field('pjFunction')}
|
||||
${field('bop')}
|
||||
${field('dateCode')}
|
||||
${field('produceRegion')}
|
||||
</div>
|
||||
|
||||
<!-- Process Info -->
|
||||
<div class="lot-detail-section">
|
||||
<div class="lot-detail-section-title">製程資訊</div>
|
||||
${field('workcenterGroup')}
|
||||
${field('workcenter')}
|
||||
${field('spec')}
|
||||
${field('specSequence')}
|
||||
${field('workflow')}
|
||||
${field('equipment')}
|
||||
${field('equipmentCount')}
|
||||
${field('location')}
|
||||
</div>
|
||||
|
||||
<!-- Material Info -->
|
||||
<div class="lot-detail-section">
|
||||
<div class="lot-detail-section-title">物料資訊</div>
|
||||
${field('waferLotId')}
|
||||
${field('waferPn')}
|
||||
${field('waferLotPrefix')}
|
||||
${field('leadframeName')}
|
||||
${field('leadframeOption')}
|
||||
${field('compoundName')}
|
||||
${field('dieConsumption')}
|
||||
${field('uts')}
|
||||
</div>
|
||||
|
||||
<!-- Hold Info (if HOLD status) -->
|
||||
${data.wipStatus === 'HOLD' || data.holdCount > 0 ? `
|
||||
<div class="lot-detail-section">
|
||||
<div class="lot-detail-section-title">Hold 資訊</div>
|
||||
${field('holdReason')}
|
||||
${field('holdCount')}
|
||||
${field('holdEmp')}
|
||||
${field('holdDept')}
|
||||
${field('holdComment')}
|
||||
${field('releaseTime')}
|
||||
${field('releaseEmp')}
|
||||
${field('releaseComment')}
|
||||
</div>
|
||||
` : ''}
|
||||
|
||||
<!-- NCR Info (if exists) -->
|
||||
${data.ncrId ? `
|
||||
<div class="lot-detail-section">
|
||||
<div class="lot-detail-section-title">NCR 資訊</div>
|
||||
${field('ncrId')}
|
||||
${field('ncrDate')}
|
||||
</div>
|
||||
` : ''}
|
||||
|
||||
<!-- Comments -->
|
||||
<div class="lot-detail-section">
|
||||
<div class="lot-detail-section-title">備註資訊</div>
|
||||
${field('comment')}
|
||||
${field('commentDate')}
|
||||
${field('commentEmp')}
|
||||
${field('futureHoldComment')}
|
||||
</div>
|
||||
|
||||
<!-- Other Info -->
|
||||
<div class="lot-detail-section">
|
||||
<div class="lot-detail-section-title">其他資訊</div>
|
||||
${field('owner')}
|
||||
${field('startDate')}
|
||||
${field('tmttRemaining')}
|
||||
${field('dataUpdateDate')}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.getElementById('lotDetailContent').innerHTML = html;
|
||||
}
|
||||
|
||||
function closeLotDetail() {
|
||||
const panel = document.getElementById('lotDetailPanel');
|
||||
panel.classList.remove('show');
|
||||
|
||||
// Remove highlight from selected row
|
||||
document.querySelectorAll('.lot-id-link').forEach(el => {
|
||||
el.classList.remove('active');
|
||||
});
|
||||
|
||||
selectedLotId = null;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Initialize
|
||||
// ============================================================
|
||||
|
||||
Reference in New Issue
Block a user