Files
andon/app/components/DotMap.vue
Julien Calixte 3e5f7d3643 feat(dashboard): red-dot weak-point map on /defects (T7)
Overlay each filed defect as a red dot inside its section box with a
diagonal date label; cap visible dots and collapse the remainder to a
"+N more" using the authoritative per-section counts (T6).

Add an `overlay` scoped slot to BoardView (C2) so the dashboard can layer
dots without forking the board definition (F1); the reporting view leaves
it empty. DotMap groups the bounded feed by section and the /defects page
wires both endpoints.
2026-05-28 00:31:17 +02:00

108 lines
2.9 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
// The weak-point dot map (C5, F4): overlays each defect as a red dot inside its
// Section box with a diagonal date label. Visible dots are capped (~812) and
// the remainder collapses to a "+N more" marker, so a hot Section stays legible
// (DESIGN §5, ADR T7 — dot positions are cosmetic; the Section is the pin).
//
// Reuses BoardView (C2) via its `overlay` slot, and the feed + per-section
// counts from T6: `defects` are the newest-first feed slice (bounded), `counts`
// the true per-section totals that drive the "+N more" arithmetic.
interface DotDefect {
id: string
sectionId: string
createdAt: string | Date
}
const props = defineProps<{
defects: DotDefect[]
counts: Record<string, number>
cap?: number
}>()
const cap = computed(() => props.cap ?? 10)
// Group the feed by section, preserving its newest-first order.
const bySection = computed(() => {
const map: Record<string, DotDefect[]> = {}
for (const d of props.defects) (map[d.sectionId] ??= []).push(d)
return map
})
const dotsFor = (id: string) => (bySection.value[id] ?? []).slice(0, cap.value)
// Hidden defects = true total (counts, authoritative) minus what we drew.
function overflowFor(id: string): number {
const total = props.counts[id] ?? bySection.value[id]?.length ?? 0
return Math.max(0, total - dotsFor(id).length)
}
// Compact, locale-free date stamp (DD/MM, UTC) for the diagonal label.
function formatDate(value: string | Date): string {
const d = new Date(value)
const dd = String(d.getUTCDate()).padStart(2, '0')
const mm = String(d.getUTCMonth() + 1).padStart(2, '0')
return `${dd}/${mm}`
}
// Cosmetic scatter: a stable 5-wide grid inside the box (ADR T7).
function dotStyle(index: number) {
return {
left: `${12 + (index % 5) * 17}%`,
top: `${20 + Math.floor(index / 5) * 22}%`,
}
}
</script>
<template>
<BoardView>
<template #overlay="{ sectionId }">
<span
v-for="(defect, index) in dotsFor(sectionId)"
:key="defect.id"
class="dot"
:style="dotStyle(index)"
data-test="dot"
>
<span class="dot__date">{{ formatDate(defect.createdAt) }}</span>
</span>
<span v-if="overflowFor(sectionId) > 0" class="dot-more" data-test="more">
+{{ overflowFor(sectionId) }} more
</span>
</template>
</BoardView>
</template>
<style scoped>
.dot {
position: absolute;
width: 0.6rem;
height: 0.6rem;
border-radius: 50%;
background: #e11d48;
/* Centre the dot on its grid point so labels splay outward consistently. */
transform: translate(-50%, -50%);
}
.dot__date {
position: absolute;
left: 0.7rem;
bottom: 0.7rem;
font-size: 0.6rem;
line-height: 1;
white-space: nowrap;
/* Diagonal label, as on the physical board. */
transform: rotate(-45deg);
transform-origin: bottom left;
}
.dot-more {
position: absolute;
right: 0.4rem;
bottom: 0.4rem;
font-size: 0.7rem;
font-weight: 600;
color: #e11d48;
}
</style>