Skip to content
This repository was archived by the owner on Sep 3, 2025. It is now read-only.

Commit 1792378

Browse files
authored
feat(ui): add dedicated status tab for can report in incident edit sheet (#5459)
* feat(ui): add dedicated status tab for can report in incident edit sheet * ui: change status to reports * tests: add component tests for timeline report tab
1 parent 6514110 commit 1792378

File tree

3 files changed

+242
-0
lines changed

3 files changed

+242
-0
lines changed

src/dispatch/static/dispatch/src/incident/EditSheet.vue

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
</template>
3535
<v-tabs color="primary" fixed-tabs v-model="tab" :disabled="id == null">
3636
<v-tab value="details"> Details </v-tab>
37+
<v-tab value="reports"> Reports </v-tab>
3738
<v-tab value="resources"> Resources </v-tab>
3839
<v-tab value="participants"> Participants </v-tab>
3940
<v-tab value="timeline"> Timeline </v-tab>
@@ -46,6 +47,9 @@
4647
<v-window-item value="details">
4748
<incident-details-tab />
4849
</v-window-item>
50+
<v-window-item value="reports">
51+
<incident-timeline-report-tab />
52+
</v-window-item>
4953
<v-window-item value="resources">
5054
<incident-resources-tab />
5155
</v-window-item>
@@ -81,6 +85,7 @@ import IncidentCostsTab from "@/incident/CostsTab.vue"
8185
import IncidentDetailsTab from "@/incident/DetailsTab.vue"
8286
import IncidentParticipantsTab from "@/incident/ParticipantsTab.vue"
8387
import IncidentResourcesTab from "@/incident/ResourcesTab.vue"
88+
import IncidentTimelineReportTab from "@/incident/TimelineReportTab.vue"
8489
import IncidentTasksTab from "@/incident/TasksTab.vue"
8590
import IncidentTimelineTab from "@/incident/TimelineTab.vue"
8691
import WorkflowInstanceTab from "@/workflow/WorkflowInstanceTab.vue"
@@ -94,6 +99,7 @@ export default {
9499
IncidentDetailsTab,
95100
IncidentParticipantsTab,
96101
IncidentResourcesTab,
102+
IncidentTimelineReportTab,
97103
IncidentTasksTab,
98104
IncidentTimelineTab,
99105
WorkflowInstanceTab,
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
<template>
2+
<v-container>
3+
<v-timeline density="compact" clipped>
4+
<v-timeline-item hide-dot>
5+
<v-row>
6+
<v-col class="text-right text-caption">(times in UTC)</v-col>
7+
</v-row>
8+
</v-timeline-item>
9+
<v-timeline-item
10+
v-for="event in tacticalReports"
11+
:key="event.id"
12+
:icon="'mdi-text-box-check'"
13+
class="mb-4"
14+
dot-color="blue"
15+
>
16+
<template #icon>
17+
<v-icon color="white" />
18+
</template>
19+
<v-row justify="space-between">
20+
<v-col cols="7">
21+
{{ event.description }}
22+
<v-card v-if="event.details">
23+
<v-card-title class="text-subtitle-1">Conditions</v-card-title>
24+
<v-card-text>{{ event.details.conditions }}</v-card-text>
25+
<v-card-title class="text-subtitle-1">Actions</v-card-title>
26+
<v-card-text>{{ event.details.actions }}</v-card-text>
27+
<v-card-title class="text-subtitle-1">Needs</v-card-title>
28+
<v-card-text>{{ event.details.needs }}</v-card-text>
29+
</v-card>
30+
<div class="text-caption">
31+
{{ event.source }}
32+
</div>
33+
</v-col>
34+
<v-col class="text-right" cols="4">
35+
<v-tooltip location="bottom">
36+
<template #activator="{ props }">
37+
<span v-bind="props" class="wavy-underline">{{
38+
formatToUTC(event.started_at)
39+
}}</span>
40+
</template>
41+
<span class="pre-formatted">{{ formatToTimeZones(event.started_at) }}</span>
42+
</v-tooltip>
43+
</v-col>
44+
</v-row>
45+
</v-timeline-item>
46+
</v-timeline>
47+
</v-container>
48+
</template>
49+
50+
<script>
51+
import { mapFields } from "vuex-map-fields"
52+
import { formatToUTC, formatToTimeZones } from "@/filters"
53+
54+
export default {
55+
name: "IncidentTimelineReportTab",
56+
57+
setup() {
58+
return { formatToUTC, formatToTimeZones }
59+
},
60+
61+
computed: {
62+
...mapFields("incident", ["selected.events"]),
63+
64+
tacticalReports() {
65+
return this.events
66+
.filter((event) => event.description.includes("created a new tactical report"))
67+
.sort((a, b) => new Date(a.started_at) - new Date(b.started_at))
68+
},
69+
},
70+
}
71+
</script>
72+
73+
<style scoped src="@/styles/timeline.css" />
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
import { mount, flushPromises } from "@vue/test-utils"
2+
import { expect, test, vi, beforeEach } from "vitest"
3+
import { createVuetify } from "vuetify"
4+
import * as components from "vuetify/components"
5+
import * as directives from "vuetify/directives"
6+
import { createStore } from "vuex"
7+
import { getField } from "vuex-map-fields"
8+
import TimelineReportTab from "@/incident/TimelineReportTab.vue"
9+
10+
// Mock the filters
11+
vi.mock("@/filters", () => ({
12+
formatToUTC: vi.fn((date) => date),
13+
formatToTimeZones: vi.fn((date) => date),
14+
}))
15+
16+
const vuetify = createVuetify({
17+
components,
18+
directives,
19+
})
20+
21+
global.ResizeObserver = require("resize-observer-polyfill")
22+
23+
// Mock data
24+
const mockEvents = [
25+
{
26+
id: 1,
27+
started_at: "2023-01-01T10:00:00Z",
28+
source: "Incident Participant",
29+
description: "John Doe created a new tactical report",
30+
details: {
31+
conditions: "Current conditions",
32+
actions: "Planned actions",
33+
needs: "Resource needs",
34+
},
35+
},
36+
{
37+
id: 2,
38+
started_at: "2023-01-01T11:00:00Z",
39+
source: "Incident Participant",
40+
description: "Jane Smith created a new tactical report",
41+
details: {
42+
conditions: "Updated conditions",
43+
actions: "New actions",
44+
needs: "Additional needs",
45+
},
46+
},
47+
{
48+
id: 3,
49+
started_at: "2023-01-01T12:00:00Z",
50+
source: "Other Source",
51+
description: "This is not a tactical report",
52+
details: {},
53+
},
54+
]
55+
56+
// Create a proper Vuex store mock
57+
function createMockStore(events = mockEvents) {
58+
return createStore({
59+
modules: {
60+
incident: {
61+
namespaced: true,
62+
state: {
63+
selected: {
64+
events: events,
65+
},
66+
},
67+
getters: {
68+
getField, // This is required for vuex-map-fields to work
69+
},
70+
},
71+
},
72+
})
73+
}
74+
75+
// Helper function to create wrapper
76+
function createWrapper(events = mockEvents) {
77+
const store = createMockStore(events)
78+
return mount(TimelineReportTab, {
79+
global: {
80+
plugins: [vuetify, store],
81+
},
82+
})
83+
}
84+
85+
beforeEach(() => {
86+
vi.clearAllMocks()
87+
})
88+
89+
test("mounts correctly and displays tactical reports", async () => {
90+
const wrapper = createWrapper()
91+
await flushPromises()
92+
93+
const timelineItems = wrapper.findAllComponents({ name: "v-timeline-item" })
94+
expect(timelineItems.length).toBe(3) // 2 tactical reports + 1 header item
95+
})
96+
97+
test("filters out non-tactical report events", async () => {
98+
const wrapper = createWrapper()
99+
await flushPromises()
100+
101+
const descriptions = wrapper.findAll(".v-col").map((col) => col.text())
102+
expect(descriptions.filter((d) => d.includes("tactical report")).length).toBe(2)
103+
expect(descriptions.filter((d) => d.includes("not a tactical report")).length).toBe(0)
104+
})
105+
106+
test("displays event details correctly", async () => {
107+
const wrapper = createWrapper()
108+
await flushPromises()
109+
110+
const cards = wrapper.findAllComponents({ name: "v-card" })
111+
const firstCard = cards[0]
112+
113+
expect(firstCard.text()).toContain("Current conditions")
114+
expect(firstCard.text()).toContain("Planned actions")
115+
expect(firstCard.text()).toContain("Resource needs")
116+
})
117+
118+
test("sorts events chronologically", async () => {
119+
const wrapper = createWrapper([...mockEvents].reverse())
120+
await flushPromises()
121+
122+
const descriptions = wrapper.findAll(".v-col").map((col) => col.text())
123+
const tacticalReports = descriptions.filter((d) => d.includes("tactical report"))
124+
125+
expect(tacticalReports[0]).toContain("John Doe")
126+
expect(tacticalReports[1]).toContain("Jane Smith")
127+
})
128+
129+
test("displays UTC time notice", async () => {
130+
const wrapper = createWrapper()
131+
await flushPromises()
132+
133+
const utcNotice = wrapper.find(".text-caption")
134+
expect(utcNotice.text()).toContain("UTC")
135+
})
136+
137+
test("handles empty events array", async () => {
138+
const wrapper = createWrapper([])
139+
await flushPromises()
140+
141+
const timelineItems = wrapper.findAllComponents({ name: "v-timeline-item" })
142+
expect(timelineItems.length).toBe(1) // Just the header item
143+
})
144+
145+
test("displays correct icon for tactical reports", async () => {
146+
const wrapper = createWrapper()
147+
await flushPromises()
148+
149+
const timelineItems = wrapper.findAllComponents({ name: "v-timeline-item" })
150+
// Subtract 1 for the header item which doesn't have an icon
151+
const itemsWithIcons = timelineItems.length - 1
152+
153+
expect(itemsWithIcons).toBe(2) // Should have 2 tactical reports with icons
154+
155+
const icons = wrapper.findAll(".v-icon")
156+
// Filter out any utility icons (like those in tooltips)
157+
const tacticalReportIcons = icons.filter((icon) => {
158+
const classes = icon.classes()
159+
return classes.includes("mdi-text-box-check")
160+
})
161+
162+
expect(tacticalReportIcons.length).toBeGreaterThan(0)
163+
})

0 commit comments

Comments
 (0)