fix(resource-status): sort machine names, fix LOT click, support multi-select matrix filter

- Sort level-2 resource nodes alphabetically in status matrix hierarchy
- Fix LOT_COUNT using raw row count when no valid RUNCARDLOTID exists,
  causing LOT badge to render but click to silently fail
- Change matrix cell filter from single-select to multi-select (OR logic)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
egg
2026-02-10 18:25:20 +08:00
parent 9a4e08810b
commit d033ffeb26
3 changed files with 573 additions and 581 deletions

View File

@@ -40,7 +40,7 @@ const filterState = reactive({
isMonitor: false,
});
const matrixFilter = ref(null);
const matrixFilter = ref([]);
const summaryStatusFilter = ref(null);
const hierarchyState = reactive({});
@@ -146,7 +146,7 @@ async function loadEquipment() {
const data = unwrapApiResult(result, '載入設備資料失敗');
allEquipment.value = Array.isArray(data) ? data : [];
matrixFilter.value = null;
matrixFilter.value = [];
summaryStatusFilter.value = null;
resetHierarchyState();
}
@@ -185,11 +185,7 @@ async function checkCacheStatus() {
}
}
function buildMatrixFilterLabel(filter) {
if (!filter) {
return '';
}
function buildSingleFilterLabel(filter) {
const parts = [filter.workcenter_group];
if (filter.family) {
parts.push(filter.family);
@@ -198,28 +194,15 @@ function buildMatrixFilterLabel(filter) {
const resource = allEquipment.value.find((item) => item.RESOURCEID === filter.resource);
parts.push(resource?.RESOURCENAME || filter.resource);
}
parts.push(STATUS_DISPLAY_MAP[filter.status] || filter.status);
return `矩陣篩選: ${parts.join(' / ')}`;
return parts.join(' / ');
}
function isSameMatrixFilter(left, right) {
if (!left || !right) {
return false;
}
return (
(left.workcenter_group || null) === (right.workcenter_group || null) &&
(left.status || null) === (right.status || null) &&
(left.family || null) === (right.family || null) &&
(left.resource || null) === (right.resource || null)
);
function filterKey(f) {
return `${f.workcenter_group}|${f.status}|${f.family || ''}|${f.resource || ''}`;
}
function matchMatrixFilter(eq, filter) {
if (!filter) {
return true;
}
function matchSingleFilter(eq, filter) {
if ((eq.WORKCENTER_GROUP || 'UNKNOWN') !== filter.workcenter_group) {
return false;
}
@@ -229,13 +212,13 @@ function matchMatrixFilter(eq, filter) {
if (filter.resource && (eq.RESOURCEID || null) !== filter.resource) {
return false;
}
return normalizeStatus(eq.EQUIPMENTASSETSSTATUS) === filter.status;
}
const displayedEquipment = computed(() => {
const filters = matrixFilter.value;
return allEquipment.value.filter((eq) => {
if (matrixFilter.value && !matchMatrixFilter(eq, matrixFilter.value)) {
if (filters.length > 0 && !filters.some((f) => matchSingleFilter(eq, f))) {
return false;
}
if (summaryStatusFilter.value && normalizeStatus(eq.EQUIPMENTASSETSSTATUS) !== summaryStatusFilter.value) {
@@ -248,9 +231,9 @@ const displayedEquipment = computed(() => {
const activeFilterText = computed(() => {
const labels = [];
const matrixLabel = buildMatrixFilterLabel(matrixFilter.value);
if (matrixLabel) {
labels.push(matrixLabel);
if (matrixFilter.value.length > 0) {
const parts = matrixFilter.value.map(buildSingleFilterLabel);
labels.push(`矩陣篩選: ${parts.join(' + ')}`);
}
if (summaryStatusFilter.value) {
@@ -261,20 +244,23 @@ const activeFilterText = computed(() => {
});
function applyMatrixFilter(nextFilter) {
if (isSameMatrixFilter(matrixFilter.value, nextFilter)) {
matrixFilter.value = null;
return;
}
matrixFilter.value = {
const entry = {
workcenter_group: nextFilter.workcenter_group,
status: nextFilter.status,
family: nextFilter.family || null,
resource: nextFilter.resource || null,
};
const key = filterKey(entry);
const idx = matrixFilter.value.findIndex((f) => filterKey(f) === key);
if (idx >= 0) {
matrixFilter.value = matrixFilter.value.filter((_, i) => i !== idx);
} else {
matrixFilter.value = [...matrixFilter.value, entry];
}
}
function clearAllEquipmentFilters() {
matrixFilter.value = null;
matrixFilter.value = [];
summaryStatusFilter.value = null;
}

View File

@@ -19,8 +19,8 @@ const props = defineProps({
default: () => ({}),
},
matrixFilter: {
type: Object,
default: null,
type: Array,
default: () => [],
},
});
@@ -84,17 +84,19 @@ function calcOuPct(counts) {
return (Number(counts.PRD || 0) / denominator) * 100;
}
function isMatrixFilterMatch(filter, { group, status, family = null, resource = null }) {
if (!filter) {
function isMatrixFilterMatch(filters, { group, status, family = null, resource = null }) {
if (!filters || filters.length === 0) {
return false;
}
const sameGroup = filter.workcenter_group === group;
const sameStatus = filter.status === status;
const sameFamily = (filter.family || null) === (family || null);
const sameResource = (filter.resource || null) === (resource || null);
return sameGroup && sameStatus && sameFamily && sameResource;
return filters.some((f) => {
return (
f.workcenter_group === group &&
f.status === status &&
(f.family || null) === (family || null) &&
(f.resource || null) === (resource || null)
);
});
}
function buildMatrixHierarchy(equipment) {
@@ -155,6 +157,10 @@ function buildMatrixHierarchy(equipment) {
});
groupNode.children.forEach((familyNode) => {
familyNode.children.sort((left, right) =>
String(left.name).localeCompare(String(right.name), 'zh-Hant')
);
familyNode.selectedColumns = Object.fromEntries(
MATRIX_STATUS_COLUMNS.map((status) => [
status,

View File

@@ -294,7 +294,7 @@ def _aggregate_by_resourceid(records: list[dict[str, Any]]) -> list[dict[str, An
'CAUSECODE': first.get('CAUSECODE'),
'REPAIRCODE': first.get('REPAIRCODE'),
# LOT related fields
'LOT_COUNT': len(seen_lots) if seen_lots else len(group),
'LOT_COUNT': len(lot_details),
'LOT_DETAILS': lot_details, # LOT details for tooltip
'TOTAL_TRACKIN_QTY': total_qty,
'LATEST_TRACKIN_TIME': latest_trackin,