Files
lucia-frontend/src/components/Discover/Conformance/ConformanceResults.vue
2023-09-19 13:33:32 +08:00

701 lines
27 KiB
Vue

<template>
<section class="p-4 mr-0.5 space-y-2 h-full w-[calc(100vw_-_316px)] overflow-y-auto scrollbar float-right">
<div v-show="isCoverPlate" class="w-[calc(100vw_-_300px)] h-screen-main fixed bottom-0 right-0 bg-gradient-to-tr from-neutral-500/50 to-neutral-900/50 z-[1]">
</div>
<!-- title -->
<p class="h2 text-base">Conformance Checking Results ({{ data.total }})<span class="material-symbols-outlined text-base align-middle ml-2" v-tooltip.bottom="tooltip.results" type="text">info</span></p>
<!-- total group -->
<ul class=" text-neutral-10 text-sm flex gap-2 py-2">
<li class=" bg-cfm-primary rounded-full px-4 py-1 space-x-2">
<span class="material-symbols-outlined text-base align-middle mr-2">check_circle</span>Conforming<span>{{ data.counts.conforming }}</span>
</li>
<li class=" bg-cfm-secondary rounded-full px-4 py-1 space-x-2">
<span class="material-symbols-outlined text-base align-middle mr-2">cancel</span>Not Conforming<span>{{ data.counts.not_conforming }}</span>
</li>
<li class=" bg-neutral-700 rounded-full px-4 py-1 space-x-2" v-show="data.counts.not_applicable != 0">
<iconNA class="inline-block mr-1"></iconNA>Not Applicable<span>{{ data.counts.not_applicable }}</span>
</li>
</ul>
<!-- chart -->
<div class="flex gap-4">
<div class="border rounded border-neutral-300 p-2 bg-neutral-10 w-1/2">
<p class="p-2 flex justify-between items-center">
<div>
<span class="block text-sm font-bold mb-2">Conformance Rate<span class="material-symbols-outlined text-sm align-middle ml-2" v-tooltip.bottom="tooltip.rate">info</span></span>
<small class="text-neutral-700 font-normal block">{{ data.charts.rate.xMin }} ~ {{ data.charts.rate.xMax }}</small>
</div>
<span class="text-2xl font-bold">{{ data.charts.rate.rate }}%</span>
</p>
<Chart type="line" :data="rateChartData" :options="rateChartOptions" class="w-[99%]"/>
</div>
<div class="border rounded border-neutral-300 p-2 bg-neutral-10 w-1/2">
<p class="p-2 flex justify-between items-center">
<div>
<span class="block text-sm font-bold mb-2">Cases<span class="material-symbols-outlined text-sm align-middle ml-2">info</span></span>
<small class="text-neutral-700 font-normal block">{{ data.charts.cases.xMin }} ~ {{ data.charts.cases.xMax }}</small>
</div>
<span class="text-2xl font-bold"><span class="text-cfm-primary">{{ data.charts.cases.conforming }}</span>&nbsp/&nbsp{{ data.charts.cases.total }}</span>
</p>
<Chart type="bar" :data="casesChartData" :options="casesChartOptions" class="w-[99%]"/>
</div>
<!-- Fitness 暫時不做 basis-1/3 basis-1/2 -->
<!-- <div class="border rounded border-neutral-300 p-2 bg-neutral-10 basis-1/3">
<p class="h2 pl-2 flex justify-between items-center">
<span>Fitness<span class="material-symbols-outlined text-sm align-middle ml-2">info</span></span>
<span class="text-2xl">{{ data.charts.fitness }}</span>
</p>
</div> -->
</div>
<!-- effect -->
<section>
<p class="h2 text-base">Effect</p>
<div class="flex gap-4 w-full">
<div class="border rounded border-neutral-300 p-2 bg-neutral-10 w-1/2">
<p class="h2 pl-2 mb-2">Throughput Time</p>
<div v-if="data.effect.time !== null">
<p class="pl-2 space-x-2" v-if="data.effect.time.not_conforming === null">
<span>All cases are conforming to set rules. Average throughput time is</span>
<span class="text-cfm-primary text-2xl font-medium">{{ data.effect.time.conforming }}</span>
<span>days.</span>
</p>
<p class="pl-2 space-x-2" v-else-if="data.effect.time.conforming === null">
<span>None of the cases is conforming to set rules. Average throughput time is</span>
<span class="text-cfm-secondary text-2xl font-medium">{{ data.effect.time.not_conforming }}</span>
<span>days.</span>
</p>
<p class="pl-2 space-x-2 max-w-full" v-else>
<span class="text-cfm-primary text-2xl font-medium inline-block">{{ data.effect.time.conforming }}</span>
<span>vs</span>
<span class="text-cfm-secondary text-2xl font-medium inline-block">{{ data.effect.time.not_conforming }}</span>
<span>days,</span>
<span class="text-2xl font-medium inline-block">{{ data.effect.time.difference }}</span>
<span>days of difference.</span>
</p>
</div>
</div>
<div class="border rounded border-neutral-300 p-2 bg-neutral-10 w-1/2">
<p class="h2 pl-2 mb-2">Activities per Case</p>
<div v-if="data.effect.tasks !== null">
<p class="pl-2 space-x-2" v-if="data.effect.tasks.not_conforming === null">
<span>All cases are conforming to set rules. Average activities in per cases is</span>
<span class="text-cfm-primary text-2xl font-medium">{{ data.effect.tasks.conforming }}</span> .
</p>
<p class="pl-2 space-x-2" v-else-if="data.effect.tasks.conforming === null">
<span>None of the cases is conforming to set rules. Average activities in per cases is</span>
<span class="text-cfm-secondary text-2xl font-medium">{{ data.effect.tasks.not_conforming }}</span> .
</p>
<p class="pl-2 space-x-2 max-w-full" v-else>
<span class="text-cfm-primary text-2xl font-medium inline-block">{{ data.effect.tasks.conforming }}</span>
<span>vs</span>
<span class="text-cfm-secondary text-2xl font-medium inline-block">{{ data.effect.tasks.not_conforming }}</span>
<span>activities,</span>
<span class="text-2xl font-medium inline-block">{{ data.effect.tasks.difference }}</span>
<span>activities of difference.</span>
</p>
</div>
</div>
</div>
</section>
<!-- Loop group -->
<section>
<div v-if="data.loops == null"></div>
<div v-else>
<p class="h2 text-base">Loop List</p>
<div class="border rounded border-neutral-300 p-2 bg-neutral-10 w-full">
<p class="h2 pl-2 mb-2">Short Loop(s)</p>
<table class="text-sm min-w-full table-fixed">
<tbody>
<tr v-for="(trace, key) in data.loops" :key="key">
<td class="p-2 pl-6 truncate max-w-0 w-1/3">
<span class="material-symbols-outlined disc text-sm align-middle mr-1">fiber_manual_record</span>{{ trace.label }}
</td>
<td class="p-2 min-w-[96px] w-2/5">
<div class="h-4 w-full bg-neutral-300 rounded-sm overflow-hidden">
<div class="h-full bg-primary" :style="trace.value"></div>
</div>
</td>
<td class="p-2 text-right truncate">{{ trace.count }}</td>
<td class="p-2 text-center">{{ trace.ratio }}%</td>
<td class="p-2 text-center">
<div class="btn btn-sm btn-c-primary cursor-pointer" @click="openLoopMore(trace.no)">More</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</section>
<!-- Issues group -->
<section>
<div v-if="data.issues == null || data.issues.length == 0"></div>
<div v-else>
<p class="h2 text-base">Non-conformance Issues</p>
<div class="flex gap-4 w-full">
<!-- Issues chart -->
<div v-if="data.timeTrend.chart != null" class="border rounded border-neutral-300 p-2 bg-neutral-10 w-1/2">
<p class="h2 pl-2 flex justify-between items-center">
<span>Time Trend<span class="material-symbols-outlined text-sm align-middle ml-2" v-tooltip.bottom="tooltip.rate">info</span></span>
<span class="text-2xl"><span class="text-cfm-secondary">{{ data.timeTrend.not_conforming }}</span>/{{ data.timeTrend.total }}</span>
</p>
<Chart type="line" :data="timeChartData" :options="timeChartOptions" class="w-[99%]"/>
</div>
<!-- Issues list -->
<div v-if="data.issues.length === 0" class="w-1/2"></div>
<div v-else class="border rounded border-neutral-300 p-2 bg-neutral-10 " :class="data.timeTrend.chart !== null ? 'w-1/2' : 'w-full'">
<p class="h2 pl-2 mb-2">Issue List</p>
<table class="text-sm min-w-full table-fixed" v-if="data.issues !== 'reset'">
<tbody>
<tr v-for="(trace, key) in data.issues" :key="key">
<td class="p-2 pl-6 truncate max-w-0 w-1/3">
<span class="material-symbols-outlined disc text-sm align-middle mr-1">fiber_manual_record</span>{{ trace.label }}
</td>
<td class="p-2 min-w-[96px] w-2/5">
<div class="h-4 w-full bg-neutral-300 rounded-sm overflow-hidden">
<div class="h-full bg-cfm-secondary" :style="trace.value"></div>
</div>
</td>
<td class="p-2 text-right truncate">{{ trace.count }}</td>
<td class="p-2 text-center">{{ trace.ratio }}%</td>
<td class="p-2 text-center">
<div class="btn btn-sm btn-cfm-secondary cursor-pointer" @click="openMore(trace.no)">More</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</section>
<MoreModal :listModal="issuesModal" @closeModal="issuesModal = $event" :listTraces="issueTraces" :taskSeq="taskSeq" :cases="cases" :listNo="issuesNo" :traceId="traceId" :firstCases="firstCases" :category="'issue'"></MoreModal>
<MoreModal :listModal="loopModal" @closeModal="loopModal = $event" :listTraces="loopTraces" :taskSeq="loopTaskSeq" :cases="loopCases" :listNo="loopNo" :traceId="looptraceId" :firstCases="loopFirstCases" :category="'loop'"></MoreModal>
</section>
</template>
<script>
import { storeToRefs } from 'pinia';
import ConformanceStore from '@/stores/conformance.js';
import iconNA from '@/components/icons/IconNA.vue';
import MoreModal from './MoreModal.vue';
import getNumberLabel from '@/module/numberLabel.js';
import { setLineChartData, setBarChartData, timeRange, yTimeRange, getXIndex, formatTime } from '@/module/setChartData.js';
import abbreviateNumber from '@/module/abbreviateNumber.js';
import getMoment from 'moment';
export default {
setup() {
const conformanceStore = ConformanceStore();
const { conformanceTempReportData, issueTraces, taskSeq, cases, loopTraces, loopTaskSeq, loopCases } = storeToRefs(conformanceStore);
return { conformanceTempReportData, issueTraces, taskSeq, cases, loopTraces, loopTaskSeq, loopCases, conformanceStore }
},
data() {
return {
data: {
total: '--',
counts: {
conforming: '--',
not_conforming: '--',
not_applicable: 0,
},
charts: {
rate: {
rate: '--',
chart: {},
},
cases: {
conforming: '--',
total: '--',
chart: {},
},
fitness: '--',
},
effect: {
time: null,
tasks: null,
},
loops: null,
issues: 'reset',
timeTrend: {
not_conforming: '--',
total: '--',
chart: {},
},
},
isCoverPlate: false,
issuesModal: false,
loopModal: false,
rateChartData: null,
rateChartOptions: null,
casesChartData: null,
casesChartOptions: null,
timeChartData: null,
timeChartOptions: null,
issuesNo: null,
traceId: null,
firstCases: null,
loopNo: null,
looptraceId: null,
loopFirstCases: null,
selectDurationTime: null,
tooltip: {
results: {
value: 'This page will perform a conformance check based on the filtering results of the map.',
},
rate: {
value: '=(total Non-Conformance/total Conformance)*100%'
},
timeTrend: {
value: '=Not Conforming / (total Conforming+total Not Conforming)'
}
}
}
},
components: {
iconNA,
MoreModal,
},
watch: {
conformanceTempReportData: {
handler: function(newValue) {
this.data = this.setConformanceTempReportData(newValue);
},
}
},
methods: {
/**
* set progress bar width
* @param {number} value
* @returns {string} 樣式的寬度設定
*/
progressWidth(value){
return `width:${value}%;`
},
/**
* Number to percentage
* @param {number} val
* @returns {string} 轉換完成的百分比字串
*/
getPercentLabel(val){
return (val * 100 === 100) ? (val * 100) : (val * 100).toFixed(1);
},
/**
* Convert seconds to days
* @param {number} sec
* @returns {number} day
*/
convertSecToDay(sec) {
return (sec / 86400)
},
/**
* Open Issues Modal.
* @param {number} no trace no
*/
async openMore(no) {
// async await 解決非同步資料延遲傳遞導致未讀取到而出錯的問題
this.issuesNo = no;
await this.conformanceStore.getLogConformanceIssue(no);
this.traceId = await this.issueTraces[0].id;
this.firstCases = await this.conformanceStore.getLogConformanceTraceDetail(no, this.issueTraces[0].id, 0);
this.issuesModal = await true;
},
/**
* Open Loop Modal.
* @param {number} no trace no
*/
async openLoopMore(no) {
// async await 解決非同步資料延遲傳遞導致未讀取到而出錯的問題
this.loopNo = no;
await this.conformanceStore.getLogConformanceLoop(no);
this.looptraceId = await this.loopTraces[0].id;
this.loopFirstCases = await this.conformanceStore.getLogConformanceLoopsTraceDetail(no, this.loopTraces[0].id, 0);
this.loopModal = await true;
},
/**
* set conformance report data
* @param {object} data new watch's value
*/
setConformanceTempReportData(data){
let total = getNumberLabel(Object.values(data.counts).reduce((acc, val) => acc + val, 0));
let sum = data.counts.conforming + data.counts.not_conforming;
let rate = ((data.counts.conforming / sum) * 100).toFixed(1);
let isNullTime = value => value === null ? null : getNumberLabel((value / 86400).toFixed(1));
let isNullCase = value => value === null ? null : getNumberLabel(value.toFixed(1));
let setLoopData = value => value.map(item => {
return {
no: item.no,
label: item.description,
value: `width:${this.getPercentLabel(item.count / data.counts.conforming)}%;`,
count: item.count,
ratio: this.getPercentLabel(item.count / data.counts.conforming),
}
});
let setIssueData = value => value.map(item => {
return {
no: item.no,
label: item.description,
value: `width:${this.getPercentLabel(item.count / data.counts.not_conforming)}%;`,
count: item.count,
ratio: this.getPercentLabel(item.count / data.counts.not_conforming),
}
});
let isNullLoops = value => value === null ? null : setLoopData(value);
let isNullIsssue = value => value === null ? null : setIssueData(value);
let result = {
total: `Total ${total}`,
counts: {
conforming: getNumberLabel(data.counts.conforming),
not_conforming: getNumberLabel(data.counts.not_conforming),
not_applicable: getNumberLabel(data.counts.not_applicable),
},
charts: {
rate: {
rate: rate,
data: setLineChartData(data.charts.rate.data, data.charts.rate.x_axis.max, data.charts.rate.x_axis.min, true),
xMax: getMoment(data.charts.rate.x_axis.max).format('YYYY/M/D'),
xMin: getMoment(data.charts.rate.x_axis.min).format('YYYY/M/D'),
},
cases: {
conforming: getNumberLabel(data.counts.conforming),
total: getNumberLabel(sum),
data: {
conforming: setBarChartData(data.charts.cases.data.filter(item => item.label === 'conforming').map(item => item.data)[0]),
not_conforming: setBarChartData(data.charts.cases.data.filter(item => item.label === 'not-conforming').map(item => item.data)[0]),
},
xMax: getMoment(data.charts.cases.x_axis.max).format('YYYY/M/D'),
xMin: getMoment(data.charts.cases.x_axis.min).format('YYYY/M/D'),
},
fitness: getNumberLabel(data.charts.fitness),
},
effect: {
time: {
conforming: isNullTime(data.effect.time.conforming),
not_conforming: isNullTime(data.effect.time.not_conforming),
difference: (isNullTime(data.effect.time.conforming) - isNullTime(data.effect.time.not_conforming)).toFixed(1),
},
tasks: {
conforming: isNullCase(data.effect.tasks.conforming),
not_conforming: isNullCase(data.effect.tasks.not_conforming),
difference: (isNullCase(data.effect.tasks.conforming) - isNullCase(data.effect.tasks.not_conforming)).toFixed(1),
},
},
loops: isNullLoops(data.loops),
issues: isNullIsssue(data.issues),
timeTrend: {
not_conforming: getNumberLabel(data.counts.not_conforming),
total: getNumberLabel(sum),
chart: null,
xMax: null,
xMin: null,
yMax: null,
yMin: null,
}
};
if (data.charts.time) {
result.timeTrend.chart = setLineChartData(data.charts.time.data, data.charts.time.x_axis.max, data.charts.time.x_axis.min, false, data.charts.time.y_axis.max, data.charts.time.y_axis.min);
result.timeTrend.xMax = data.charts.time.x_axis.max;
result.timeTrend.xMin = data.charts.time.x_axis.min;
result.timeTrend.yMax = data.charts.time.y_axis.max;
result.timeTrend.yMin = data.charts.time.y_axis.min;
}
this.setRateChartData(result.charts.rate.data); // 建立圖表 Rate Chart.js
this.setCasesChartData(result.charts.cases.data.conforming, result.charts.cases.data.not_conforming, data.charts.cases.x_axis.max, data.charts.cases.x_axis.min); // 建立圖表 Cases Chart.js
if(data.charts.time) this.setTimeChartData(result.timeTrend.chart, result.timeTrend.xMax, result.timeTrend.xMin, result.timeTrend.yMax, result.timeTrend.yMax, result.timeTrend.yMin); // 建立圖表 Time Chart.js
return result;
},
/**
* set Rate Chart Data
* @param {object} data new rate chart data
*/
setRateChartData(data){
this.rateChartData = {
labels:[],
datasets: [
{
label: 'Rate',
data: data,
fill: false,
pointRadius: 0, // 隱藏點
pointHoverRadius: 0, // 隱藏點的 hover
tension: 0.4,
borderColor: '#0099FF',
x: 'x',
y: 'y',
}
]
};
this.rateChartOptions = {
responsive: true,
maintainAspectRatio: false,
aspectRatio: 0.6,
layout: {
padding: {
top: 16,
left: 8,
right: 8,
}
},
plugins: {
legend: false, // 圖例
tooltip: {
enabled: false // 隱藏 工具提示框
}
},
scales: {
x: {
type: 'time',
// time: {
// max: '2022-01-12T02:29:42',
// min: '2022-01-03T00:56:25',
// displayFormats: {
// day: 'yyyy/M/d'
// }
// },
ticks: {
display: false,
// maxRotation: 0, // 不旋轉 lable 0~50
// color: '#334155',
// source: 'data',
// align: 'inner', // label 在軸線的位置
// callback: function(value, index, values) {
// return (index === 0 || index === values.length - 1) ? getMoment(value).format('YYYY/M/D') : null;
// },
},
grid: {
display: false, // 隱藏 x 軸網格
},
border: {
color: '#334155',
},
},
y: {
beginAtZero: true, // scale 包含 0
suggestedMin: 0,
suggestedMax: 1,
ticks:{ // 設定間隔數值
includeBounds: true,
color: '#334155',
align: 'inner',
callback: function(value, index, values) {
if (value === 0) return `${value * 100}%`;
else if (value === 1) return `${value * 100}%`;
},
},
grid: {
display: false, // 隱藏 y 軸網格
},
border: {
color: '#334155',
},
},
},
};
},
/**
* set Cases Chart Data
* @param {array} data new cases chart conforming data
* @param {array} data new cases chart not conforming data
* @param {number} data new cases chart xMax
* @param {number} data new cases chart xMin
*/
setCasesChartData(conformingData, notConformingData, xMax, xMin){
this.casesChartData = {
datasets: [
{
type: 'bar',
label: 'Conforming',
data: conformingData,
backgroundColor: '#0099FF',
},
{
type: 'bar',
label: 'Not Conforming',
data: notConformingData,
backgroundColor: '#FFAA44',
},
]
};
this.casesChartOptions = {
responsive: true,
maintainAspectRatio: false,
aspectRatio: 0.8,
layout: {
padding: {
top: 16,
left: 8,
right: 8,
}
},
plugins: {
tooltips: {
mode: 'index',
intersect: false
},
legend: false, // 圖例
},
scales: {
x: {
stacked: true,
ticks: {
display: false,
// autoSkip: false,
// maxRotation: 0, // 不旋轉 lable 0~50
// color: '#334155',
// align: 'center', // label 在軸線的位置
// callback: function(value, index, values) {
// if(index === 0) return getMoment(xMin).format('yyyy/M/D');
// else if(index === values.length - 1) return getMoment(xMax).format('yyyy/M/D');
// else return null;
// },
},
grid: {
display: false, // 隱藏 x 軸網格
},
border: {
color: '#334155',
},
},
y: {
stacked: true,
beginAtZero: true, // scale 包含 0
ticks:{
color: '#334155',
align: 'inner',
callback: function(value, index, values) {
if (index === 0) return abbreviateNumber(value);
else if (index === values.length - 1) return abbreviateNumber(value);
},
},
grid: {
// display: false, // 隱藏 y 軸網格
color: function(context) {
return context.tick.value === 0 ? '#334155' : null;
},
drawTicks: false,
},
border: {
color: '#334155',
},
},
},
};
},
/**
* set Time Trend chart data
* @param {array} data Time Trend chart conforming data
* @param {number} xMax Time Trend xMax
* @param {number} xMin Time Trend xMin
* @param {number} yMax Time Trend yMax
* @param {number} yMin Time Trend yMin
*/
setTimeChartData(data, xMax, xMin, yMax, yMin) {
let max = yMax * 1.1;
let xVal = timeRange(xMin, xMax, 100);
let yVal = yTimeRange(data, 100, yMin, yMax);
data = xVal.map((x, index) => ({ x, y: yVal[index] }));
let formattedXVal = xVal.map(value => formatTime(value));
let selectTimeMinIndex = getXIndex(xVal, this.selectDurationTime.min);
let selectTimeMaxIndex = getXIndex(xVal, this.selectDurationTime.max);
const start = selectTimeMinIndex;
const end = selectTimeMaxIndex;
const inside = (ctx, value) => ctx.p0DataIndex >= start && ctx.p1DataIndex <= end ? value : undefined;
const outside = (ctx, value) => ctx.p0DataIndex < start || ctx.p1DataIndex > end ? value : undefined;
this.timeChartData = {
labels: formattedXVal,
datasets: [
{
label: 'Conforming',
data: yVal,
fill: true,
showLine: false,
tension: 0.4,
backgroundColor: 'rgba(0,153,255)',
pointRadius: 0,
pointHitRadius: 0,
spanGaps: true,
segment: {
backgroundColor: ctx => inside(ctx, 'rgb(0,153,255)') || outside(ctx, 'rgb(255,170,68)'),
},
x: 'x',
y: 'y',
},
]
};
this.timeChartOptions = {
responsive: true,
maintainAspectRatio: false,
layout: {
padding: {
top: 16,
left: 8,
right: 8,
}
},
plugins: {
legend: false, // 圖例
tooltip: false,
},
scales: {
x: {
ticks: {
// autoSkip: false, // 彈性顯示或全部顯示 lable
// autoSkipPadding: 4, // lable 之間的距離
maxRotation: 0, // 不旋轉 lable 0~50
color: '#334155',
display: true,
},
grid: {
display: false, // 隱藏 x 軸網格
},
title: {
display: true,
text: 'Time',
color: 'rgba(100,116,139)',
},
},
y: {
beginAtZero: true, // scale 包含 0
max: max,
ticks: { // 設定間隔數值
display: false, // 隱藏數值,只顯示格線
stepSize: max / 4,
},
grid: {
color: 'rgba(100,116,139)',
drawTicks: false // 隱藏左側多的空間
},
border: {
display: false, // 隱藏左側多出來的線
},
title: {
display: true,
text: 'Occurrences',
color: 'rgba(100,116,139)',
},
},
},
};
},
},
created() {
this.$emitter.on('coverPlate', boolean => {
this.isCoverPlate = boolean;
});
// 取得 selectTimeTange 給 Tiem Trend 使用
this.$emitter.on('timeRangeMaxMin', data => this.selectDurationTime = data);
},
}
</script>
<style scoped>
:deep(.disc) {
font-variation-settings:
'FILL' 1,
'wght' 100,
'GRAD' 0,
'opsz' 20
}
</style>