@nexxtmove/ui 0.1.24 → 0.1.26

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/nuxt.js CHANGED
@@ -1,20 +1,23 @@
1
- import { defineNuxtModule as a, addComponent as c, addTemplate as s } from "@nuxt/kit";
2
- const i = "components/AnimatedNumber/AnimatedNumber.vue", m = "components/Button/Button.vue", x = "components/Calendar/Calendar.vue", p = "components/Chip/Chip.vue", u = "components/DatePicker/DatePicker.vue", d = "components/Header/Header.vue", r = "components/Icon/Icon.vue", l = "components/InfoBlock/InfoBlock.vue", v = "components/ProgressBar/ProgressBar.vue", S = "components/SocialIcons/SocialIcons.vue", N = "components/SocialMediaCustomTemplate/SocialMediaCustomTemplate.vue", f = "components/SocialMediaTemplate/SocialMediaTemplate.vue", C = "components/SocialMediaType/SocialMediaType.vue", M = "components/StepperHeader/StepperHeader.vue", T = {
3
- NexxtAnimatedNumber: i,
1
+ import { defineNuxtModule as c, addComponent as s, addTemplate as i } from "@nuxt/kit";
2
+ const a = "components/AnimatedNumber/AnimatedNumber.vue", m = "components/Button/Button.vue", x = "components/Calendar/Calendar.vue", p = "components/Chip/Chip.vue", l = "components/DatePicker/DatePicker.vue", u = "components/Header/Header.vue", d = "components/Icon/Icon.vue", r = "components/InfoBlock/InfoBlock.vue", v = "components/ProgressBar/ProgressBar.vue", N = "components/SocialIcons/SocialIcons.vue", S = "components/SocialMediaCustomTemplate/SocialMediaCustomTemplate.vue", T = "components/SocialMediaTemplate/SocialMediaTemplate.vue", f = "components/SocialMediaType/SocialMediaType.vue", C = "components/StepperHeader/StepperHeader.vue", h = "components/TimelineEvent/TimelineEvent.vue", M = "components/TimelinePhaseblock/TimelinePhaseblock.vue", P = "components/Tooltip/Tooltip.vue", b = {
3
+ NexxtAnimatedNumber: a,
4
4
  NexxtButton: m,
5
5
  NexxtCalendar: x,
6
6
  NexxtChip: p,
7
- NexxtDatePicker: u,
8
- NexxtHeader: d,
9
- NexxtIcon: r,
10
- NexxtInfoBlock: l,
7
+ NexxtDatePicker: l,
8
+ NexxtHeader: u,
9
+ NexxtIcon: d,
10
+ NexxtInfoBlock: r,
11
11
  NexxtProgressBar: v,
12
- NexxtSocialIcons: S,
13
- NexxtSocialMediaCustomTemplate: N,
14
- NexxtSocialMediaTemplate: f,
15
- NexxtSocialMediaType: C,
16
- NexxtStepperHeader: M
17
- }, I = a({
12
+ NexxtSocialIcons: N,
13
+ NexxtSocialMediaCustomTemplate: S,
14
+ NexxtSocialMediaTemplate: T,
15
+ NexxtSocialMediaType: f,
16
+ NexxtStepperHeader: C,
17
+ NexxtTimelineEvent: h,
18
+ NexxtTimelinePhaseblock: M,
19
+ NexxtTooltip: P
20
+ }, B = c({
18
21
  meta: {
19
22
  name: "@nexxtmove/ui",
20
23
  configKey: "nexxtmoveUi",
@@ -26,13 +29,13 @@ const i = "components/AnimatedNumber/AnimatedNumber.vue", m = "components/Button
26
29
  addCSS: !0
27
30
  },
28
31
  setup(e, o) {
29
- for (const [t, n] of Object.entries(T))
30
- c({
32
+ for (const [t, n] of Object.entries(b))
33
+ s({
31
34
  name: t,
32
35
  export: "default",
33
36
  filePath: `@nexxtmove/ui/${n}`
34
37
  });
35
- e.addCSS && (s({
38
+ e.addCSS && (i({
36
39
  filename: "nexxtmove-ui.css",
37
40
  getContents: () => `
38
41
  @import "tailwindcss";
@@ -45,5 +48,5 @@ const i = "components/AnimatedNumber/AnimatedNumber.vue", m = "components/Button
45
48
  }
46
49
  });
47
50
  export {
48
- I as default
51
+ B as default
49
52
  };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@nexxtmove/ui",
3
3
  "type": "module",
4
- "version": "0.1.24",
4
+ "version": "0.1.26",
5
5
  "exports": {
6
6
  ".": {
7
7
  "types": "./dist/index.d.ts",
@@ -1,6 +1,10 @@
1
1
  <script setup lang="ts">
2
2
  import { computed, ref, watch } from 'vue'
3
3
 
4
+ defineOptions({
5
+ name: 'NexxtAnimatedNumber',
6
+ })
7
+
4
8
  type EasingFunction = 'ease-linear' | 'ease-in' | 'ease-out' | 'ease-in-out'
5
9
 
6
10
  const {
@@ -0,0 +1,291 @@
1
+ import type { Meta, StoryObj } from '@storybook/vue3-vite'
2
+ import TimelineEvent from './TimelineEvent.vue'
3
+
4
+ export default {
5
+ title: 'Components/Atoms/Timeline Event',
6
+ component: TimelineEvent,
7
+ } satisfies Meta<typeof TimelineEvent>
8
+
9
+ type Story = StoryObj<typeof TimelineEvent>
10
+
11
+ const baseEvent = {
12
+ id: 1,
13
+ happened_at: '2024-06-15T10:30:00.000Z',
14
+ medium_type: null,
15
+ url: null,
16
+ page_title: null,
17
+ request_reason: null,
18
+ agenda_item_type: null,
19
+ source: 'system',
20
+ direction: 'outbound',
21
+ description: null,
22
+ }
23
+
24
+ export const ContactCreated: Story = {
25
+ args: {
26
+ event: {
27
+ ...baseEvent,
28
+ type: 'contact_created',
29
+ },
30
+ splitView: true,
31
+ },
32
+ }
33
+
34
+ export const InboundEvent: Story = {
35
+ args: {
36
+ event: {
37
+ ...baseEvent,
38
+ type: 'contact_requested',
39
+ direction: 'inbound',
40
+ },
41
+ splitView: true,
42
+ },
43
+ }
44
+
45
+ export const PageViewed: Story = {
46
+ args: {
47
+ event: {
48
+ ...baseEvent,
49
+ type: 'page_viewed',
50
+ url: 'https://example.com/page',
51
+ page_title: 'Example Page',
52
+ description: 'Example Page',
53
+ },
54
+ splitView: true,
55
+ },
56
+ }
57
+
58
+ export const WhatsAppMessageReceived: Story = {
59
+ args: {
60
+ event: {
61
+ ...baseEvent,
62
+ type: 'whatsapp_message_received',
63
+ },
64
+ splitView: true,
65
+ },
66
+ }
67
+
68
+ export const WhatsAppMessageSent: Story = {
69
+ args: {
70
+ event: {
71
+ ...baseEvent,
72
+ type: 'whatsapp_message_sent',
73
+ direction: 'outbound',
74
+ },
75
+ splitView: true,
76
+ },
77
+ }
78
+
79
+ export const WithUrl: Story = {
80
+ args: {
81
+ event: {
82
+ ...baseEvent,
83
+ type: 'proposal_sent',
84
+ url: 'https://example.com/proposal/123',
85
+ description: 'Voorstel Q2 2024',
86
+ },
87
+ splitView: true,
88
+ },
89
+ }
90
+
91
+ export const WithRequestReason: Story = {
92
+ args: {
93
+ event: {
94
+ ...baseEvent,
95
+ type: 'house_viewing_requested',
96
+ request_reason: 'Interesse in de woning na open dag',
97
+ },
98
+ splitView: true,
99
+ },
100
+ }
101
+
102
+ export const SplitViewDisabled: Story = {
103
+ args: {
104
+ event: {
105
+ ...baseEvent,
106
+ type: 'contact_created',
107
+ direction: 'inbound',
108
+ },
109
+ splitView: false,
110
+ },
111
+ }
112
+
113
+ export const ViewingAgendaItemScheduled: Story = {
114
+ args: {
115
+ event: {
116
+ ...baseEvent,
117
+ type: 'viewing_agenda_item_scheduled',
118
+ },
119
+ splitView: true,
120
+ },
121
+ }
122
+
123
+ export const ProposalAccepted: Story = {
124
+ args: {
125
+ event: {
126
+ ...baseEvent,
127
+ type: 'proposal_accepted',
128
+ },
129
+ splitView: true,
130
+ },
131
+ }
132
+
133
+ export const KeyHandoverScheduled: Story = {
134
+ args: {
135
+ event: {
136
+ ...baseEvent,
137
+ type: 'key_handover_agenda_item_scheduled',
138
+ },
139
+ splitView: true,
140
+ },
141
+ }
142
+
143
+ export const AutomationPhaseOpened: Story = {
144
+ args: {
145
+ event: { ...baseEvent, type: 'automation_phase_opened' },
146
+ splitView: true,
147
+ },
148
+ }
149
+
150
+ export const AutomationWhatsAppButtonClicked: Story = {
151
+ args: {
152
+ event: { ...baseEvent, type: 'automation_whatsapp_button_clicked' },
153
+ splitView: true,
154
+ },
155
+ }
156
+
157
+ export const AutomationButtonMailAnswered: Story = {
158
+ args: {
159
+ event: { ...baseEvent, type: 'automation_button_mail_answered' },
160
+ splitView: true,
161
+ },
162
+ }
163
+
164
+ export const WaitingListSignup: Story = {
165
+ args: {
166
+ event: { ...baseEvent, type: 'waiting_list_signup' },
167
+ splitView: true,
168
+ },
169
+ }
170
+
171
+ export const ViewingCreated: Story = {
172
+ args: {
173
+ event: { ...baseEvent, type: 'viewing_created' },
174
+ splitView: true,
175
+ },
176
+ }
177
+
178
+ export const GeneralAgendaItemScheduled: Story = {
179
+ args: {
180
+ event: { ...baseEvent, type: 'general_agenda_item_scheduled' },
181
+ splitView: true,
182
+ },
183
+ }
184
+
185
+ export const SalesAgendaItemScheduled: Story = {
186
+ args: {
187
+ event: { ...baseEvent, type: 'sales_agenda_item_scheduled' },
188
+ splitView: true,
189
+ },
190
+ }
191
+
192
+ export const AgendaItemScheduled: Story = {
193
+ args: {
194
+ event: { ...baseEvent, type: 'agenda_item_scheduled' },
195
+ splitView: true,
196
+ },
197
+ }
198
+
199
+ export const AppraisalAgendaItemScheduled: Story = {
200
+ args: {
201
+ event: { ...baseEvent, type: 'appraisal_agenda_item_scheduled' },
202
+ splitView: true,
203
+ },
204
+ }
205
+
206
+ export const PurchaseAgendaItemScheduled: Story = {
207
+ args: {
208
+ event: { ...baseEvent, type: 'purchase_agenda_item_scheduled' },
209
+ splitView: true,
210
+ },
211
+ }
212
+
213
+ export const StructuralInspectionAgendaItemScheduled: Story = {
214
+ args: {
215
+ event: { ...baseEvent, type: 'structural_inspection_agenda_item_scheduled' },
216
+ splitView: true,
217
+ },
218
+ }
219
+
220
+ export const ValuationAgendaItemScheduled: Story = {
221
+ args: {
222
+ event: { ...baseEvent, type: 'valuation_agenda_item_scheduled' },
223
+ splitView: true,
224
+ },
225
+ }
226
+
227
+ export const MortgageAgendaItemScheduled: Story = {
228
+ args: {
229
+ event: { ...baseEvent, type: 'mortgage_agenda_item_scheduled' },
230
+ splitView: true,
231
+ },
232
+ }
233
+
234
+ export const PurchaseDeedSigningAgendaItemScheduled: Story = {
235
+ args: {
236
+ event: { ...baseEvent, type: 'purchase_deed_signing_agenda_item_scheduled' },
237
+ splitView: true,
238
+ },
239
+ }
240
+
241
+ export const ProposalOpened: Story = {
242
+ args: {
243
+ event: { ...baseEvent, type: 'proposal_opened' },
244
+ splitView: true,
245
+ },
246
+ }
247
+
248
+ export const FinalInspectionAgendaItemScheduled: Story = {
249
+ args: {
250
+ event: { ...baseEvent, type: 'final_inspection_agenda_item_scheduled' },
251
+ splitView: true,
252
+ },
253
+ }
254
+
255
+ export const TransportAgendaItemScheduled: Story = {
256
+ args: {
257
+ event: { ...baseEvent, type: 'transport_agenda_item_scheduled' },
258
+ splitView: true,
259
+ },
260
+ }
261
+
262
+ export const NewsletterOpened: Story = {
263
+ args: {
264
+ event: { ...baseEvent, type: 'newsletter_opened' },
265
+ splitView: true,
266
+ },
267
+ }
268
+
269
+ export const HouseValuationRequested: Story = {
270
+ args: {
271
+ event: { ...baseEvent, type: 'house_valuation_requested' },
272
+ splitView: true,
273
+ },
274
+ }
275
+
276
+ export const Unsubscribed: Story = {
277
+ args: {
278
+ event: { ...baseEvent, type: 'unsubscribed' },
279
+ splitView: true,
280
+ },
281
+ }
282
+
283
+ export const UnknownEventType: Story = {
284
+ args: {
285
+ event: {
286
+ ...baseEvent,
287
+ type: 'custom_event_type',
288
+ },
289
+ splitView: true,
290
+ },
291
+ }
@@ -0,0 +1,166 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import { mount } from '@vue/test-utils'
3
+ import TimelineEvent from './TimelineEvent.vue'
4
+
5
+ const baseEvent = {
6
+ id: 1,
7
+ happened_at: '2024-06-15T10:30:00.000Z',
8
+ medium_type: null,
9
+ url: null,
10
+ page_title: null,
11
+ request_reason: null,
12
+ agenda_item_type: null,
13
+ source: 'system',
14
+ direction: 'outbound',
15
+ description: null,
16
+ }
17
+
18
+ const mountEvent = (eventOverrides = {}, props = {}) =>
19
+ mount(TimelineEvent, {
20
+ props: {
21
+ event: { ...baseEvent, type: 'contact_created', ...eventOverrides },
22
+ ...props,
23
+ },
24
+ global: {
25
+ stubs: {
26
+ NexxtIcon: true,
27
+ },
28
+ },
29
+ })
30
+
31
+ describe('TimelineEvent', () => {
32
+ it('renders the formatted date', () => {
33
+ const wrapper = mountEvent()
34
+ const dateText = wrapper.find('.extra-small-normal').text()
35
+ expect(dateText).toContain('om')
36
+ })
37
+
38
+ it('sets data-event-source attribute from direction', () => {
39
+ const wrapper = mountEvent({ direction: 'inbound' })
40
+ expect(wrapper.attributes('data-event-source')).toBe('inbound')
41
+ })
42
+
43
+ it('sets data-event-date attribute from happened_at', () => {
44
+ const wrapper = mountEvent({ happened_at: '2024-06-15T10:30:00.000Z' })
45
+ expect(wrapper.attributes('data-event-date')).toBe('2024-06-15T10:30:00.000Z')
46
+ })
47
+
48
+ it('sets date-event-type attribute from type', () => {
49
+ const wrapper = mountEvent({ type: 'page_viewed' })
50
+ expect(wrapper.attributes('date-event-type')).toBe('page_viewed')
51
+ })
52
+
53
+ it('renders the event label for a known event type', () => {
54
+ const wrapper = mountEvent({ type: 'contact_created' })
55
+ expect(wrapper.text()).toContain('Contact aangemaakt')
56
+ })
57
+
58
+ it('renders the event type as label for an unknown event type', () => {
59
+ const wrapper = mountEvent({ type: 'custom_unknown_type' })
60
+ expect(wrapper.text()).toContain('custom_unknown_type')
61
+ })
62
+
63
+ it('does not render a link when event.url is null', () => {
64
+ const wrapper = mountEvent({ url: null })
65
+ expect(wrapper.find('a').exists()).toBe(false)
66
+ })
67
+
68
+ it('renders a link when event.url is set', () => {
69
+ const wrapper = mountEvent({ url: 'https://example.com' })
70
+ const link = wrapper.find('a')
71
+ expect(link.exists()).toBe(true)
72
+ expect(link.attributes('href')).toBe('https://example.com')
73
+ expect(link.attributes('target')).toBe('_blank')
74
+ })
75
+
76
+ it('renders event.description as link text when set', () => {
77
+ const wrapper = mountEvent({ url: 'https://example.com', description: 'Mijn voorstel' })
78
+ expect(wrapper.find('a').text()).toContain('Mijn voorstel')
79
+ })
80
+
81
+ it('renders event.url as link text when description is null', () => {
82
+ const wrapper = mountEvent({ url: 'https://example.com', description: null })
83
+ expect(wrapper.find('a').text()).toContain('https://example.com')
84
+ })
85
+
86
+ it('does not render request_reason when not set', () => {
87
+ const wrapper = mountEvent({ request_reason: null })
88
+ expect(wrapper.text()).not.toContain('Reden:')
89
+ })
90
+
91
+ it('renders request_reason when set', () => {
92
+ const wrapper = mountEvent({ request_reason: 'Interesse in de woning' })
93
+ expect(wrapper.text()).toContain('Reden: Interesse in de woning')
94
+ })
95
+
96
+ it('aligns outbound events to the left by default (no justify-end)', () => {
97
+ const wrapper = mountEvent({ direction: 'outbound' }, { splitView: true })
98
+ expect(wrapper.classes()).not.toContain('justify-end')
99
+ })
100
+
101
+ it('aligns inbound events to the right when splitView is true', () => {
102
+ const wrapper = mountEvent({ direction: 'inbound' }, { splitView: true })
103
+ expect(wrapper.classes()).toContain('justify-end')
104
+ })
105
+
106
+ it('does not align inbound events to the right when splitView is false', () => {
107
+ const wrapper = mountEvent({ direction: 'inbound' }, { splitView: false })
108
+ expect(wrapper.classes()).not.toContain('justify-end')
109
+ })
110
+
111
+ it('reverses icon and content order for inbound events in splitView', () => {
112
+ const wrapper = mountEvent({ direction: 'inbound' }, { splitView: true })
113
+ const innerRow = wrapper.find('.flex.w-full.shrink-0')
114
+ expect(innerRow.classes()).toContain('flex-row-reverse')
115
+ })
116
+
117
+ it('does not reverse order for outbound events', () => {
118
+ const wrapper = mountEvent({ direction: 'outbound' }, { splitView: true })
119
+ const innerRow = wrapper.find('.flex.w-full.shrink-0')
120
+ expect(innerRow.classes()).not.toContain('flex-row-reverse')
121
+ })
122
+
123
+ it('applies max-w-lg to content when splitView is true', () => {
124
+ const wrapper = mountEvent({}, { splitView: true })
125
+ const content = wrapper.find('.rounded-xl')
126
+ expect(content.classes()).toContain('max-w-lg')
127
+ })
128
+
129
+ it('does not apply max-w-lg to content when splitView is false', () => {
130
+ const wrapper = mountEvent({}, { splitView: false })
131
+ const content = wrapper.find('.rounded-xl')
132
+ expect(content.classes()).not.toContain('max-w-lg')
133
+ })
134
+
135
+ it('defaults splitView to true', () => {
136
+ const wrapper = mount(TimelineEvent, {
137
+ props: {
138
+ event: { ...baseEvent, type: 'contact_created', direction: 'inbound' },
139
+ },
140
+ global: { stubs: { NexxtIcon: true } },
141
+ })
142
+ expect(wrapper.classes()).toContain('justify-end')
143
+ })
144
+
145
+ it('applies correct background color class for known event type', () => {
146
+ const wrapper = mountEvent({ type: 'contact_created' })
147
+ const iconWrapper = wrapper.find('.rounded-full')
148
+ expect(iconWrapper.classes()).toContain('bg-pink-500')
149
+ })
150
+
151
+ it('applies default background color for unknown event type', () => {
152
+ const wrapper = mountEvent({ type: 'unknown_type' })
153
+ const iconWrapper = wrapper.find('.rounded-full')
154
+ expect(iconWrapper.classes()).toContain('bg-gray-500')
155
+ })
156
+
157
+ it('aligns date text to the right for inbound events in splitView', () => {
158
+ const wrapper = mountEvent({ direction: 'inbound' }, { splitView: true })
159
+ expect(wrapper.find('.extra-small-normal').classes()).toContain('text-right')
160
+ })
161
+
162
+ it('does not align date text to the right for outbound events', () => {
163
+ const wrapper = mountEvent({ direction: 'outbound' }, { splitView: true })
164
+ expect(wrapper.find('.extra-small-normal').classes()).not.toContain('text-right')
165
+ })
166
+ })
@@ -0,0 +1,115 @@
1
+ <script lang="ts" setup>
2
+ import NexxtIcon from '../Icon/Icon.vue'
3
+ import { computed } from 'vue'
4
+
5
+ defineOptions({
6
+ name: 'NexxtTimelineEvent',
7
+ })
8
+ import { EVENT_CONFIG_MAP, type TimelineEvent as Event } from './TimelineTypes'
9
+
10
+ interface EventConfig {
11
+ label: string
12
+ icon: string
13
+ color: string
14
+ }
15
+
16
+ const defaultEventConfig: EventConfig = {
17
+ label: 'Gebeurtenis',
18
+ icon: 'info',
19
+ color: 'bg-gray-500',
20
+ }
21
+
22
+ const eventConfig = computed(() => {
23
+ return EVENT_CONFIG_MAP[props.event.type] || { ...defaultEventConfig, label: props.event.type }
24
+ })
25
+
26
+ const props = withDefaults(
27
+ defineProps<{
28
+ event: Event
29
+ splitView?: boolean
30
+ }>(),
31
+ {
32
+ splitView: true,
33
+ },
34
+ )
35
+
36
+ const formattedDate = computed(() => {
37
+ const date = new Date(props.event.happened_at)
38
+ const datePart = date.toLocaleDateString('nl-NL', {
39
+ day: 'numeric',
40
+ month: 'short',
41
+ year: 'numeric',
42
+ })
43
+ const timePart = date.toLocaleTimeString('nl-NL', {
44
+ hour: '2-digit',
45
+ minute: '2-digit',
46
+ })
47
+
48
+ return `${datePart} om ${timePart}`
49
+ })
50
+
51
+ const isContactEvent = computed(() => {
52
+ return props.splitView && props.event.direction === 'inbound'
53
+ })
54
+ </script>
55
+
56
+ <template>
57
+ <div
58
+ :data-event-source="event.direction"
59
+ :data-event-date="event.happened_at"
60
+ :date-event-type="event.type"
61
+ class="relative z-20 flex flex-wrap items-center gap-x-6 gap-y-2"
62
+ :class="{
63
+ 'justify-end': isContactEvent,
64
+ }"
65
+ >
66
+ <div
67
+ class="w-full px-24 extra-small-normal text-gray-600"
68
+ :class="{
69
+ 'text-right': isContactEvent,
70
+ }"
71
+ >
72
+ {{ formattedDate }}
73
+ </div>
74
+
75
+ <div
76
+ class="flex w-full shrink-0 items-center gap-6"
77
+ :class="{
78
+ 'flex-row-reverse': isContactEvent,
79
+ }"
80
+ >
81
+ <div
82
+ class="flex h-10 w-10 shrink-0 items-center justify-center rounded-full"
83
+ :class="[eventConfig.color]"
84
+ >
85
+ <NexxtIcon
86
+ :name="eventConfig.icon as any"
87
+ :type="eventConfig.icon === 'whatsapp' ? 'brands' : 'light'"
88
+ class="flex h-6 w-6 text-xl text-white"
89
+ />
90
+ </div>
91
+ <div
92
+ class="flex w-full flex-col gap-2 rounded-xl border border-gray-200 p-8"
93
+ :class="{ 'max-w-lg': splitView }"
94
+ >
95
+ <span class="heading-3">
96
+ {{ eventConfig.label }}
97
+ </span>
98
+ <a
99
+ v-if="event.url"
100
+ :href="event.url"
101
+ target="_blank"
102
+ class="group mt-1 flex justify-between gap-2 text-sm text-cornflower-blue-500 hover:text-cornflower-blue-600"
103
+ >
104
+ <span class="truncate group-hover:underline">
105
+ {{ event.description || event.url }}
106
+ </span>
107
+ <NexxtIcon name="external-link" class="ml-1 h-4 w-4 text-gray-950" />
108
+ </a>
109
+ <span v-if="event.request_reason" class="mt-1 text-sm text-gray-500">
110
+ Reden: {{ event.request_reason }}
111
+ </span>
112
+ </div>
113
+ </div>
114
+ </div>
115
+ </template>