fix(query-tool): export button hover visibility, jobs tab hide columns and export with txn history

- Increase CSS specificity for .btn-export to prevent portal-shell override on hover
- Remove RESOURCEID and CONTAINERIDS from jobs tab display columns
- Add lot_jobs_with_txn.sql joining JOB with JOBTXNHISTORY for complete export
- Route lot_jobs export through get_lot_jobs_with_history() for full transaction data

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
egg
2026-02-24 19:10:19 +08:00
parent 7fffa812a3
commit e37902861f
6 changed files with 729 additions and 598 deletions

View File

@@ -24,7 +24,6 @@ ensureMesApiAvailable();
const JOB_COLUMN_PRIORITY = Object.freeze([
'JOBID',
'RESOURCEID',
'RESOURCENAME',
'JOBSTATUS',
'JOBMODELNAME',
@@ -42,7 +41,6 @@ const JOB_COLUMN_PRIORITY = Object.freeze([
'PJ_SYMPTOMCODE2NAME',
'CREATE_EMPNAME',
'COMPLETE_EMPNAME',
'CONTAINERIDS',
'CONTAINERNAMES',
]);

View File

@@ -116,16 +116,17 @@ body {
cursor: not-allowed;
}
.btn-export {
.btn.btn-export {
background: #0f766e;
color: #fff;
}
.btn-export:hover {
.btn.btn-export:hover {
background: #0b5e59;
color: #fff;
}
.btn-export:disabled {
.btn.btn-export:disabled {
opacity: 0.6;
cursor: not-allowed;
}

View File

@@ -27,6 +27,7 @@ from mes_dashboard.services.query_tool_service import (
get_lot_holds,
get_lot_splits,
get_lot_jobs,
get_lot_jobs_with_history,
get_lot_associations_batch,
get_equipment_status_hours,
get_equipment_lots,
@@ -683,7 +684,7 @@ def export_csv():
filename = f'lot_splits_{container_id}.csv'
elif export_type == 'lot_jobs':
result = get_lot_jobs(
result = get_lot_jobs_with_history(
params.get('equipment_id'),
params.get('time_start'),
params.get('time_end')

View File

@@ -1411,6 +1411,66 @@ def get_lot_jobs(
return {'error': f'查詢失敗: {str(exc)}'}
def get_lot_jobs_with_history(
equipment_id: str,
time_start: str,
time_end: str
) -> Dict[str, Any]:
"""Get JOB records with full transaction history for export.
Joins DW_MES_JOB with DW_MES_JOBTXNHISTORY so each row contains
both job-level and transaction-level columns, matching the pattern
used by the job-query export.
Args:
equipment_id: Equipment ID (RESOURCEID)
time_start: Start time (ISO format)
time_end: End time (ISO format)
Returns:
Dict with 'data' (flattened job+txn records) and 'total', or 'error'.
"""
if not all([equipment_id, time_start, time_end]):
return {'error': '請指定設備和時間範圍'}
try:
if isinstance(time_start, str):
start = datetime.strptime(time_start, '%Y-%m-%d %H:%M:%S')
else:
start = time_start
if isinstance(time_end, str):
end = datetime.strptime(time_end, '%Y-%m-%d %H:%M:%S')
else:
end = time_end
sql = SQLLoader.load("query_tool/lot_jobs_with_txn")
params = {
'equipment_id': equipment_id,
'time_start': start,
'time_end': end,
}
df = read_sql_df(sql, params)
data = _df_to_records(df)
logger.debug(
f"LOT jobs with txn history: {len(data)} records for {equipment_id}"
)
return {
'data': data,
'total': len(data),
'equipment_id': equipment_id,
}
except Exception as exc:
logger.error(
f"LOT jobs with txn history query failed for {equipment_id}: {exc}"
)
return {'error': f'查詢失敗: {str(exc)}'}
# ============================================================
# Equipment Period Query Functions
# ============================================================

View File

@@ -0,0 +1,38 @@
-- LOT Related JOB Records with Transaction History Export
-- Joins JOB with JOBTXNHISTORY for complete CSV export
--
-- Parameters:
-- :equipment_id - Equipment ID (EQUIPMENTID = RESOURCEID)
-- :time_start - Start time of LOT processing
-- :time_end - End time of LOT processing
SELECT
j.RESOURCENAME,
j.JOBID,
j.JOBSTATUS AS JOB_FINAL_STATUS,
j.JOBMODELNAME,
j.JOBORDERNAME,
j.CREATEDATE AS JOB_CREATEDATE,
j.COMPLETEDATE AS JOB_COMPLETEDATE,
j.CAUSECODENAME AS JOB_CAUSECODENAME,
j.REPAIRCODENAME AS JOB_REPAIRCODENAME,
j.SYMPTOMCODENAME AS JOB_SYMPTOMCODENAME,
h.TXNDATE,
h.FROMJOBSTATUS,
h.JOBSTATUS AS TXN_JOBSTATUS,
h.STAGENAME,
h.CAUSECODENAME AS TXN_CAUSECODENAME,
h.REPAIRCODENAME AS TXN_REPAIRCODENAME,
h.SYMPTOMCODENAME AS TXN_SYMPTOMCODENAME,
h.USER_NAME,
h.EMP_NAME,
h.COMMENTS
FROM DWH.DW_MES_JOB j
JOIN DWH.DW_MES_JOBTXNHISTORY h ON j.JOBID = h.JOBID
WHERE j.RESOURCEID = :equipment_id
AND (
(j.CREATEDATE BETWEEN :time_start AND :time_end)
OR (j.COMPLETEDATE BETWEEN :time_start AND :time_end)
OR (j.CREATEDATE <= :time_start AND (j.COMPLETEDATE IS NULL OR j.COMPLETEDATE >= :time_end))
)
ORDER BY j.JOBID, h.TXNDATE

View File

@@ -1027,6 +1027,39 @@ class TestExportCsvBatchEndpoint:
data = response.get_json()
assert 'CONTAINERID' in data.get('error', '')
@patch('mes_dashboard.routes.query_tool_routes.get_lot_jobs_with_history')
def test_export_lot_jobs_calls_with_history(self, mock_jobs_hist, client):
"""lot_jobs export should call get_lot_jobs_with_history (includes txn)."""
mock_jobs_hist.return_value = {
'data': [
{
'RESOURCENAME': 'ASSY-01',
'JOBID': 'JOB-001',
'JOB_FINAL_STATUS': 'Complete',
'TXNDATE': '2026-01-15 10:00:00',
'TXN_JOBSTATUS': 'Complete',
'STAGENAME': 'Repair',
},
],
'total': 1,
}
response = client.post(
'/api/query-tool/export-csv',
json={
'export_type': 'lot_jobs',
'params': {
'equipment_id': 'EQ001',
'time_start': '2026-01-01 00:00:00',
'time_end': '2026-01-31 23:59:59',
},
},
)
assert response.status_code == 200
assert 'text/csv' in response.content_type
mock_jobs_hist.assert_called_once_with('EQ001', '2026-01-01 00:00:00', '2026-01-31 23:59:59')
class TestEquipmentListEndpoint:
"""Tests for /api/query-tool/equipment-list endpoint."""