Add 4D comparisons list Vue component

This commit is contained in:
D. Berge
2025-08-18 14:16:23 +02:00
parent 15af5effc3
commit 4bb087fff7

View File

@@ -0,0 +1,396 @@
<template>
<v-container fluid>
<v-data-table
:headers="headers"
:items="displayItems"
item-key="group"
:options.sync="options"
:expanded.sync="expanded"
show-expand
:loading="loading"
>
<template v-slot:item.group="{item, value}">
<v-chip
label
small
:href="`./${value}`"
>{{ value }}</v-chip>
</template>
<template v-slot:item.shots_total="{item, value}">
<div>{{ item.prime + item.other }}</div>
<v-progress-linear
background-color="secondary"
color="primary"
:value="item.prime/(item.prime+item.other)*100"
></v-progress-linear>
</template>
<template v-slot:item.prime="{item, value}">
{{ value }}
({{ (value / (item.prime + item.other) * 100).toFixed(1) }}%)
</template>
<template v-slot:item.other="{item, value}">
{{ value }}
({{ (value / (item.prime + item.other) * 100).toFixed(1) }}%)
</template>
<template v-slot:item.prod_duration="{item, value}">
<span v-if="value.days > 2" :title="`${value.days} d ${value.hours} h ${value.minutes} m ${(value.seconds + value.milliseconds/1000).toFixed(3)} s`">
{{ value.days }} d
</span>
<span v-else>
{{ value.days }} d {{ value.hours }} h {{ value.minutes }} m {{ (value.seconds + value.milliseconds/1000).toFixed(1) }} s
</span>
</template>
<template v-slot:item.prod_distance="{item, value}">
{{ (value/1000).toFixed(1) }} km
</template>
<template v-slot:item.shooting_rate_mean="{item, value}">
{{ (value).toFixed(2) }} s ±{{ (item.shooting_rate_sd).toFixed(3) }} s
</template>
<template v-slot:item.shots_per_point="{item, value}">
<div>
{{ ((item.prime + item.other)/item.points).toFixed(1) }}
({{ ((((item.prime + item.other)/item.points) / item.num_projects)*100).toFixed(2) }}%)
</div>
<v-progress-linear
:value="((((item.prime + item.other)/item.points) / item.num_projects)*100)"
></v-progress-linear>
</template>
<template v-slot:expanded-item="{ headers, item }">
<td :colspan="headers.length">
<v-data-table class="ma-1"
:headers="projectHeaders"
:items="item.projects"
dense
hide-default-footer
>
<template v-slot:item.pid="{item, value}">
<a :href="`/projects/${value}`" title="Go to project">{{ value }}</a>
</template>
<template v-slot:item.fsp="{item, value}">
<span title="First production shot">{{value.tstamp.substr(0, 10)}}</span>
</template>
<template v-slot:item.lsp="{item, value}">
<span title="Last production shot">{{value.tstamp.substr(0, 10)}}</span>
</template>
<template v-slot:item.prod_duration="{item, value}">
<span v-if="value.days > 2" :title="`${value.days} d ${value.hours} h ${value.minutes} m ${(value.seconds + value.milliseconds/1000).toFixed(3)} s`">
{{ value.days }} d
</span>
<span v-else>
{{ value.days }} d {{ value.hours }} h {{ value.minutes }} m {{ (value.seconds + value.milliseconds/1000).toFixed(1) }} s
</span>
</template>
<template v-slot:item.prod_distance="{item, value}">
{{ (value/1000).toFixed(1) }} km
</template>
</v-data-table>
</td>
</template>
</v-data-table>
</v-container>
</template>
<style>
td p:last-of-type {
margin-bottom: 0;
}
</style>
<script>
import { mapActions, mapGetters } from 'vuex';
import AccessMixin from '@/mixins/access';
// FIXME send to lib/utils or so
/*
function duration_to_ms(v) {
if (v instanceof Object) {
return (
(v.days || 0) * 86400000 +
(v.hours || 0) * 3600000 +
(v.minutes || 0) * 60000 +
(v.seconds || 0) * 1000 +
(v.milliseconds || 0)
);
} else {
return {
days: 0,
hours: 0,
minutes: 0,
seconds: 0,
milliseconds: 0
}
}
}
function ms_to_duration(v) {
const days = Math.floor(v / 86400000);
v %= 86400000;
const hours = Math.floor(v / 3600000);
v %= 3600000;
const minutes = Math.floor(v / 60000);
v %= 60000;
const seconds = Math.floor(v / 1000);
const milliseconds = v % 1000;
return { days, hours, minutes, seconds, milliseconds };
}
function normalise_duration (v) {
return ms_to_duration(duration_to_ms(v));
}
function add_durations(a, b) {
return ms_to_duration(duration_to_ms(a) + duration_to_ms(b));
}
*/
export default {
name: "GroupList",
components: {
},
mixins: [
AccessMixin
],
data () {
return {
headers: [
{
value: "group",
text: "Group name"
},
{
value: "num_projects",
text: "Number of campaigns"
},
{
value: "lines",
text: "Preplot lines"
},
{
value: "points",
text: "Preplot points"
},
{
value: "sequences",
text: "Total sequences"
},
{
value: "shots_total",
text: "Total shots"
},
{
value: "prime",
text: "Total prime"
},
{
value: "other",
text: "Total reshoot + infill"
},
/*
{
value: "ntba",
text: "Total NTBA"
},
*/
{
value: "prod_duration",
text: "Total duration"
},
{
value: "prod_distance",
text: "Total distance"
},
{
value: "shooting_rate_mean",
text: "Shooting rate (mean)"
},
{
value: "shots_per_point",
text: "Shots per point"
},
],
items: [],
expanded: [],
options: { sortBy: ["group"], sortDesc: [false] },
projectHeaders: [
{
value: "pid",
text: "ID"
},
{
value: "name",
text: "Name"
},
{
value: "fsp",
text: "Start"
},
{
value: "lsp",
text: "Finish"
},
{
value: "lines",
text: "Preplot lines"
},
{
value: "seq_final",
text: "Num. of sequences"
},
{
value: "prod_duration",
text: "Duration"
},
{
value: "prod_distance",
text: "Distance"
},
],
// Context menu stuff
contextMenuShow: false,
contextMenuX: 0,
contextMenuY: 0,
contextMenuItem: null,
/*
// FIXME Eventually need to move this into Vuex
groups: []
*/
}
},
computed: {
displayItems () {
return this.items.filter(i => i.prod_distance);
},
...mapGetters(['loading', 'groups'])
},
methods: {
/*
async prepareGroups () {
//const groups = await this.api(["/prospects"]);
//console.log("groups", groups);
const groups = {};
for (const project of this.projects) {
if (!project.prod_distance) {
// This project has no production data (either not started yet
// or production data has not been imported) so we skip it.
continue;
}
if (!project.prod_duration.days) {
project.prod_duration = normalise_duration(project.prod_duration);
}
for (const name of project.groups) {
if (!(name in groups)) {
groups[name] = {
group: name,
num_projects: 0,
lines: 0,
points: 0,
sequences: 0,
// Shots:
prime: 0,
other: 0,
ntba: 0,
prod_duration: {
days: 0,
hours: 0,
minutes: 0,
seconds: 0,
milliseconds: 0
},
prod_distance: 0,
shooting_rate: [],
projects: []
};
}
const group = groups[name];
group.num_projects++;
group.lines = Math.max(group.lines, project.lines); // In case preplots changed
group.points = Math.max(group.points, project.total); // Idem
group.sequences += project.seq_final;
group.prime += project.prime;
group.other += project.other;
//group.ntba += project.ntba;
group.prod_duration = add_durations(group.prod_duration, project.prod_duration);
group.prod_distance += project.prod_distance;
group.shooting_rate.push(project.shooting_rate);
group.projects.push(project);
}
}
this.groups = [];
for (const group of Object.values(groups)) {
group.shooting_rate_mean = d3a.mean(group.shooting_rate);
group.shooting_rate_sd = d3a.deviation(group.shooting_rate);
delete group.shooting_rate;
this.groups.push(group);
}
},
*/
async list () {
this.items = [...this.groups];
},
async load () {
await this.refreshProjects();
//await this.prepareGroups();
await this.list();
},
registerNotificationHandlers () {
this.$store.dispatch('registerHandler', {
table: 'project`',
handler: (context, message) => {
if (message.payload?.table == "public") {
this.load();
}
}
});
},
...mapActions(["api", "showSnack", "refreshProjects"])
},
mounted () {
this.registerNotificationHandlers();
this.load();
}
}
</script>