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_lot_ids,
|
||||||
search_packages,
|
search_packages,
|
||||||
search_types,
|
search_types,
|
||||||
|
get_lot_detail,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Create Blueprint
|
# Create Blueprint
|
||||||
@@ -213,6 +214,23 @@ def api_detail(workcenter: str):
|
|||||||
return jsonify({'success': False, 'error': '查詢失敗'}), 500
|
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
|
# Meta APIs
|
||||||
# ============================================================
|
# ============================================================
|
||||||
|
|||||||
@@ -20,9 +20,12 @@ logger = logging.getLogger('mes_dashboard.wip_service')
|
|||||||
|
|
||||||
|
|
||||||
def _safe_value(val):
|
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):
|
if pd.isna(val):
|
||||||
return None
|
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
|
return val
|
||||||
|
|
||||||
|
|
||||||
@@ -2145,3 +2148,276 @@ def _get_hold_detail_lots_from_oracle(
|
|||||||
import traceback
|
import traceback
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
return None
|
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);
|
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 */
|
/* Responsive */
|
||||||
@media (max-width: 1400px) {
|
@media (max-width: 1400px) {
|
||||||
.summary-row {
|
.summary-row {
|
||||||
grid-template-columns: repeat(3, 1fr);
|
grid-template-columns: repeat(3, 1fr);
|
||||||
}
|
}
|
||||||
|
.lot-detail-grid {
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 1000px) {
|
@media (max-width: 1000px) {
|
||||||
@@ -638,6 +790,9 @@
|
|||||||
.summary-row {
|
.summary-row {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
.lot-detail-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@@ -734,6 +889,21 @@
|
|||||||
<button id="btnNext" onclick="nextPage()">Next</button>
|
<button id="btnNext" onclick="nextPage()">Next</button>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
</div>
|
||||||
|
|
||||||
<!-- Loading Overlay -->
|
<!-- Loading Overlay -->
|
||||||
@@ -956,8 +1126,11 @@
|
|||||||
data.lots.forEach(lot => {
|
data.lots.forEach(lot => {
|
||||||
html += '<tr>';
|
html += '<tr>';
|
||||||
|
|
||||||
// Fixed columns
|
// Fixed columns - LOT ID is clickable
|
||||||
html += `<td class="fixed-col">${lot.lotId || '-'}</td>`;
|
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>`;
|
html += `<td class="fixed-col">${lot.equipment || '<span style="color: var(--muted);">-</span>'}</td>`;
|
||||||
|
|
||||||
// WIP Status with color and hold reason
|
// WIP Status with color and hold reason
|
||||||
@@ -1329,6 +1502,206 @@
|
|||||||
loadAllData(false);
|
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
|
// Initialize
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|||||||
Reference in New Issue
Block a user