refactor: align codebase with EventCrew domain and trim legacy band stack
- Update API: events, users, policies, routes, resources, migrations - Remove deprecated models/resources (customers, setlists, invitations, etc.) - Refresh admin app and docs; remove apps/band Made-with: Cursor
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { useEvents } from '@/composables/useEvents'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import type { UpdateEventData } from '@/types/events'
|
||||
import type { EventCrewEventStatus, UpdateEventData } from '@/types/events'
|
||||
|
||||
definePage({
|
||||
meta: {
|
||||
@@ -17,62 +17,57 @@ const eventId = computed(() => route.params.id as string)
|
||||
|
||||
const formData = ref<UpdateEventData>({})
|
||||
|
||||
const statusOptions = [
|
||||
const statusOptions: { title: string; value: EventCrewEventStatus }[] = [
|
||||
{ title: 'Draft', value: 'draft' },
|
||||
{ title: 'Pending', value: 'pending' },
|
||||
{ title: 'Confirmed', value: 'confirmed' },
|
||||
{ title: 'Completed', value: 'completed' },
|
||||
{ title: 'Cancelled', value: 'cancelled' },
|
||||
]
|
||||
|
||||
const visibilityOptions = [
|
||||
{ title: 'Private', value: 'private' },
|
||||
{ title: 'Members', value: 'members' },
|
||||
{ title: 'Public', value: 'public' },
|
||||
{ title: 'Published', value: 'published' },
|
||||
{ title: 'Registration open', value: 'registration_open' },
|
||||
{ title: 'Build-up', value: 'buildup' },
|
||||
{ title: 'Show day', value: 'showday' },
|
||||
{ title: 'Teardown', value: 'teardown' },
|
||||
{ title: 'Closed', value: 'closed' },
|
||||
]
|
||||
|
||||
const errors = ref<Record<string, string>>({})
|
||||
|
||||
// Load event data
|
||||
watch(() => eventId.value, async () => {
|
||||
if (eventId.value) {
|
||||
await fetchEvent(eventId.value)
|
||||
if (currentEvent.value) {
|
||||
formData.value = {
|
||||
title: currentEvent.value.title,
|
||||
description: currentEvent.value.description || '',
|
||||
event_date: currentEvent.value.event_date,
|
||||
start_time: currentEvent.value.start_time,
|
||||
end_time: currentEvent.value.end_time || '',
|
||||
load_in_time: currentEvent.value.load_in_time || '',
|
||||
soundcheck_time: currentEvent.value.soundcheck_time || '',
|
||||
location_id: currentEvent.value.location?.id || '',
|
||||
customer_id: currentEvent.value.customer?.id || '',
|
||||
setlist_id: currentEvent.value.setlist?.id || '',
|
||||
fee: currentEvent.value.fee || undefined,
|
||||
currency: currentEvent.value.currency,
|
||||
status: currentEvent.value.status,
|
||||
visibility: currentEvent.value.visibility,
|
||||
rsvp_deadline: currentEvent.value.rsvp_deadline || '',
|
||||
notes: currentEvent.value.notes || '',
|
||||
internal_notes: currentEvent.value.internal_notes || '',
|
||||
is_public_setlist: currentEvent.value.is_public_setlist,
|
||||
}
|
||||
if (!eventId.value) {
|
||||
return
|
||||
}
|
||||
await fetchEvent(eventId.value)
|
||||
if (currentEvent.value) {
|
||||
const e = currentEvent.value
|
||||
formData.value = {
|
||||
name: e.name,
|
||||
slug: e.slug,
|
||||
start_date: e.start_date,
|
||||
end_date: e.end_date,
|
||||
timezone: e.timezone,
|
||||
status: e.status,
|
||||
}
|
||||
}
|
||||
}, { immediate: true })
|
||||
|
||||
function flattenValidationErrors(raw: Record<string, string[] | string>): Record<string, string> {
|
||||
const out: Record<string, string> = {}
|
||||
for (const [key, val] of Object.entries(raw)) {
|
||||
out[key] = Array.isArray(val) ? val[0]! : val
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
errors.value = {}
|
||||
|
||||
try {
|
||||
await updateEvent(eventId.value, formData.value)
|
||||
router.push(`/events/${eventId.value}`)
|
||||
} catch (err: any) {
|
||||
if (err.response?.data?.errors) {
|
||||
errors.value = err.response.data.errors
|
||||
} else {
|
||||
errors.value._general = err.message || 'Failed to update event'
|
||||
try {
|
||||
await updateEvent(eventId.value, formData.value)
|
||||
await router.push(`/events/${eventId.value}`)
|
||||
}
|
||||
catch (err: unknown) {
|
||||
const ax = err as { response?: { data?: { errors?: Record<string, string[] | string>; message?: string } } }
|
||||
if (ax.response?.data?.errors) {
|
||||
errors.value = flattenValidationErrors(ax.response.data.errors)
|
||||
}
|
||||
else {
|
||||
errors.value._general = ax.response?.data?.message ?? (err instanceof Error ? err.message : 'Failed to update event')
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -98,20 +93,19 @@ async function handleSubmit() {
|
||||
<VRow>
|
||||
<VCol cols="12">
|
||||
<AppTextField
|
||||
v-model="formData.title"
|
||||
label="Title"
|
||||
placeholder="Event Title"
|
||||
:error-messages="errors.title"
|
||||
v-model="formData.name"
|
||||
label="Name"
|
||||
:error-messages="errors.name"
|
||||
required
|
||||
/>
|
||||
</VCol>
|
||||
|
||||
<VCol cols="12">
|
||||
<AppTextarea
|
||||
v-model="formData.description"
|
||||
label="Description"
|
||||
placeholder="Event Description"
|
||||
rows="3"
|
||||
<AppTextField
|
||||
v-model="formData.slug"
|
||||
label="Slug"
|
||||
:error-messages="errors.slug"
|
||||
required
|
||||
/>
|
||||
</VCol>
|
||||
|
||||
@@ -120,11 +114,10 @@ async function handleSubmit() {
|
||||
md="6"
|
||||
>
|
||||
<AppTextField
|
||||
v-model="formData.event_date"
|
||||
label="Event Date"
|
||||
v-model="formData.start_date"
|
||||
label="Start date"
|
||||
type="date"
|
||||
:error-messages="errors.event_date"
|
||||
required
|
||||
:error-messages="errors.start_date"
|
||||
/>
|
||||
</VCol>
|
||||
|
||||
@@ -133,126 +126,27 @@ async function handleSubmit() {
|
||||
md="6"
|
||||
>
|
||||
<AppTextField
|
||||
v-model="formData.start_time"
|
||||
label="Start Time"
|
||||
type="time"
|
||||
:error-messages="errors.start_time"
|
||||
required
|
||||
v-model="formData.end_date"
|
||||
label="End date"
|
||||
type="date"
|
||||
:error-messages="errors.end_date"
|
||||
/>
|
||||
</VCol>
|
||||
|
||||
<VCol
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<VCol cols="12">
|
||||
<AppTextField
|
||||
v-model="formData.end_time"
|
||||
label="End Time"
|
||||
type="time"
|
||||
v-model="formData.timezone"
|
||||
label="Timezone"
|
||||
:error-messages="errors.timezone"
|
||||
/>
|
||||
</VCol>
|
||||
|
||||
<VCol
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<AppTextField
|
||||
v-model="formData.load_in_time"
|
||||
label="Load-in Time"
|
||||
type="time"
|
||||
/>
|
||||
</VCol>
|
||||
|
||||
<VCol
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<AppTextField
|
||||
v-model="formData.soundcheck_time"
|
||||
label="Soundcheck Time"
|
||||
type="time"
|
||||
/>
|
||||
</VCol>
|
||||
|
||||
<VCol
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<AppTextField
|
||||
v-model="formData.rsvp_deadline"
|
||||
label="RSVP Deadline"
|
||||
type="datetime-local"
|
||||
/>
|
||||
</VCol>
|
||||
|
||||
<VCol
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<AppTextField
|
||||
v-model.number="formData.fee"
|
||||
label="Fee"
|
||||
type="number"
|
||||
step="0.01"
|
||||
prefix="€"
|
||||
/>
|
||||
</VCol>
|
||||
|
||||
<VCol
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<AppSelect
|
||||
v-model="formData.currency"
|
||||
label="Currency"
|
||||
:items="[{ title: 'EUR', value: 'EUR' }, { title: 'USD', value: 'USD' }]"
|
||||
/>
|
||||
</VCol>
|
||||
|
||||
<VCol
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<VCol cols="12">
|
||||
<AppSelect
|
||||
v-model="formData.status"
|
||||
label="Status"
|
||||
:items="statusOptions"
|
||||
/>
|
||||
</VCol>
|
||||
|
||||
<VCol
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<AppSelect
|
||||
v-model="formData.visibility"
|
||||
label="Visibility"
|
||||
:items="visibilityOptions"
|
||||
/>
|
||||
</VCol>
|
||||
|
||||
<VCol cols="12">
|
||||
<AppTextarea
|
||||
v-model="formData.notes"
|
||||
label="Notes"
|
||||
placeholder="Public notes"
|
||||
rows="3"
|
||||
/>
|
||||
</VCol>
|
||||
|
||||
<VCol cols="12">
|
||||
<AppTextarea
|
||||
v-model="formData.internal_notes"
|
||||
label="Internal Notes"
|
||||
placeholder="Private notes (only visible to admins)"
|
||||
rows="3"
|
||||
/>
|
||||
</VCol>
|
||||
|
||||
<VCol cols="12">
|
||||
<VCheckbox
|
||||
v-model="formData.is_public_setlist"
|
||||
label="Public Setlist"
|
||||
:error-messages="errors.status"
|
||||
/>
|
||||
</VCol>
|
||||
|
||||
@@ -271,11 +165,10 @@ async function handleSubmit() {
|
||||
color="primary"
|
||||
:loading="isLoading"
|
||||
>
|
||||
Update Event
|
||||
Save
|
||||
</VBtn>
|
||||
<VBtn
|
||||
variant="tonal"
|
||||
color="secondary"
|
||||
:to="`/events/${eventId}`"
|
||||
>
|
||||
Cancel
|
||||
@@ -287,7 +180,7 @@ async function handleSubmit() {
|
||||
</VCardText>
|
||||
</VCard>
|
||||
|
||||
<VCard v-else>
|
||||
<VCard v-else-if="isLoading">
|
||||
<VCardText class="text-center py-12">
|
||||
<VProgressCircular
|
||||
indeterminate
|
||||
@@ -297,4 +190,3 @@ async function handleSubmit() {
|
||||
</VCard>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { useEvents } from '@/composables/useEvents'
|
||||
import { useRoute } from 'vue-router'
|
||||
import InviteMembersDialog from '@/components/events/InviteMembersDialog.vue'
|
||||
|
||||
definePage({
|
||||
meta: {
|
||||
@@ -13,9 +12,7 @@ const route = useRoute()
|
||||
const { currentEvent, fetchEvent, isLoading } = useEvents()
|
||||
|
||||
const eventId = computed(() => route.params.id as string)
|
||||
const isInviteDialogOpen = ref(false)
|
||||
|
||||
// Load event
|
||||
watch(() => eventId.value, async () => {
|
||||
if (eventId.value) {
|
||||
await fetchEvent(eventId.value)
|
||||
@@ -25,29 +22,25 @@ watch(() => eventId.value, async () => {
|
||||
function getStatusColor(status: string): string {
|
||||
const colors: Record<string, string> = {
|
||||
draft: 'secondary',
|
||||
pending: 'warning',
|
||||
confirmed: 'success',
|
||||
completed: 'info',
|
||||
cancelled: 'error',
|
||||
published: 'info',
|
||||
registration_open: 'success',
|
||||
buildup: 'warning',
|
||||
showday: 'primary',
|
||||
teardown: 'warning',
|
||||
closed: 'secondary',
|
||||
}
|
||||
return colors[status] || 'secondary'
|
||||
}
|
||||
|
||||
function getRsvpColor(status: string): string {
|
||||
const colors: Record<string, string> = {
|
||||
pending: 'warning',
|
||||
available: 'success',
|
||||
unavailable: 'error',
|
||||
tentative: 'info',
|
||||
}
|
||||
return colors[status] || 'secondary'
|
||||
function formatStatusLabel(status: string): string {
|
||||
return status.replaceAll('_', ' ')
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<VCard v-if="currentEvent">
|
||||
<VCardTitle class="d-flex justify-space-between align-center">
|
||||
<VCardTitle class="d-flex justify-space-between align-center flex-wrap gap-4">
|
||||
<div>
|
||||
<div class="d-flex align-center gap-2 mb-2">
|
||||
<RouterLink
|
||||
@@ -56,31 +49,22 @@ function getRsvpColor(status: string): string {
|
||||
>
|
||||
<VIcon icon="tabler-arrow-left" />
|
||||
</RouterLink>
|
||||
<span class="text-h5">{{ currentEvent.title }}</span>
|
||||
<span class="text-h5">{{ currentEvent.name }}</span>
|
||||
</div>
|
||||
<VChip
|
||||
:color="getStatusColor(currentEvent.status)"
|
||||
size="small"
|
||||
class="mt-2"
|
||||
>
|
||||
{{ currentEvent.status_label }}
|
||||
{{ formatStatusLabel(currentEvent.status) }}
|
||||
</VChip>
|
||||
</div>
|
||||
<div class="d-flex gap-2">
|
||||
<VBtn
|
||||
:to="`/events/${eventId}/edit`"
|
||||
prepend-icon="tabler-pencil"
|
||||
>
|
||||
Edit
|
||||
</VBtn>
|
||||
<VBtn
|
||||
color="primary"
|
||||
prepend-icon="tabler-user-plus"
|
||||
@click="isInviteDialogOpen = true"
|
||||
>
|
||||
Invite Members
|
||||
</VBtn>
|
||||
</div>
|
||||
<VBtn
|
||||
:to="`/events/${eventId}/edit`"
|
||||
prepend-icon="tabler-pencil"
|
||||
>
|
||||
Edit
|
||||
</VBtn>
|
||||
</VCardTitle>
|
||||
|
||||
<VDivider />
|
||||
@@ -93,70 +77,37 @@ function getRsvpColor(status: string): string {
|
||||
>
|
||||
<div class="mb-4">
|
||||
<div class="text-body-2 text-medium-emphasis mb-1">
|
||||
Event Date
|
||||
Slug
|
||||
</div>
|
||||
<div class="text-body-1">
|
||||
{{ new Date(currentEvent.event_date).toLocaleDateString() }}
|
||||
{{ currentEvent.slug }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<div class="text-body-2 text-medium-emphasis mb-1">
|
||||
Start Time
|
||||
Start date
|
||||
</div>
|
||||
<div class="text-body-1">
|
||||
{{ currentEvent.start_time }}
|
||||
{{ new Date(currentEvent.start_date).toLocaleDateString() }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="currentEvent.end_time"
|
||||
class="mb-4"
|
||||
>
|
||||
<div class="mb-4">
|
||||
<div class="text-body-2 text-medium-emphasis mb-1">
|
||||
End Time
|
||||
End date
|
||||
</div>
|
||||
<div class="text-body-1">
|
||||
{{ currentEvent.end_time }}
|
||||
{{ new Date(currentEvent.end_date).toLocaleDateString() }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="currentEvent.location"
|
||||
class="mb-4"
|
||||
>
|
||||
<div class="mb-4">
|
||||
<div class="text-body-2 text-medium-emphasis mb-1">
|
||||
Location
|
||||
Timezone
|
||||
</div>
|
||||
<div class="text-body-1">
|
||||
{{ currentEvent.location.name }}<br>
|
||||
<span class="text-medium-emphasis">
|
||||
{{ currentEvent.location.address }}, {{ currentEvent.location.city }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="currentEvent.customer"
|
||||
class="mb-4"
|
||||
>
|
||||
<div class="text-body-2 text-medium-emphasis mb-1">
|
||||
Customer
|
||||
</div>
|
||||
<div class="text-body-1">
|
||||
{{ currentEvent.customer.name }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="currentEvent.fee"
|
||||
class="mb-4"
|
||||
>
|
||||
<div class="text-body-2 text-medium-emphasis mb-1">
|
||||
Fee
|
||||
</div>
|
||||
<div class="text-body-1">
|
||||
{{ currentEvent.currency }} {{ currentEvent.fee }}
|
||||
{{ currentEvent.timezone }}
|
||||
</div>
|
||||
</div>
|
||||
</VCol>
|
||||
@@ -166,94 +117,19 @@ function getRsvpColor(status: string): string {
|
||||
md="6"
|
||||
>
|
||||
<div
|
||||
v-if="currentEvent.description"
|
||||
v-if="currentEvent.organisation"
|
||||
class="mb-4"
|
||||
>
|
||||
<div class="text-body-2 text-medium-emphasis mb-1">
|
||||
Description
|
||||
Organisation
|
||||
</div>
|
||||
<div class="text-body-1">
|
||||
{{ currentEvent.description }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="currentEvent.notes"
|
||||
class="mb-4"
|
||||
>
|
||||
<div class="text-body-2 text-medium-emphasis mb-1">
|
||||
Notes
|
||||
</div>
|
||||
<div class="text-body-1">
|
||||
{{ currentEvent.notes }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="currentEvent.internal_notes"
|
||||
class="mb-4"
|
||||
>
|
||||
<div class="text-body-2 text-medium-emphasis mb-1">
|
||||
Internal Notes
|
||||
</div>
|
||||
<div class="text-body-1">
|
||||
{{ currentEvent.internal_notes }}
|
||||
{{ currentEvent.organisation.name }}
|
||||
</div>
|
||||
</div>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VCardText>
|
||||
|
||||
<VDivider />
|
||||
|
||||
<!-- Invitations Section -->
|
||||
<VCardText>
|
||||
<div class="d-flex justify-space-between align-center mb-4">
|
||||
<VCardTitle class="pa-0">
|
||||
Invitations ({{ currentEvent.invitations?.length || 0 }})
|
||||
</VCardTitle>
|
||||
</div>
|
||||
|
||||
<VTable v-if="currentEvent.invitations && currentEvent.invitations.length > 0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Member</th>
|
||||
<th>Email</th>
|
||||
<th>RSVP Status</th>
|
||||
<th>Response Date</th>
|
||||
<th>Note</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="invitation in currentEvent.invitations"
|
||||
:key="invitation.id"
|
||||
>
|
||||
<td>{{ invitation.user?.name || '-' }}</td>
|
||||
<td>{{ invitation.user?.email || '-' }}</td>
|
||||
<td>
|
||||
<VChip
|
||||
:color="getRsvpColor(invitation.rsvp_status)"
|
||||
size="small"
|
||||
>
|
||||
{{ invitation.rsvp_status }}
|
||||
</VChip>
|
||||
</td>
|
||||
<td>
|
||||
{{ invitation.rsvp_responded_at ? new Date(invitation.rsvp_responded_at).toLocaleString() : '-' }}
|
||||
</td>
|
||||
<td>{{ invitation.rsvp_note || '-' }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</VTable>
|
||||
|
||||
<VAlert
|
||||
v-else
|
||||
type="info"
|
||||
>
|
||||
No members invited yet. Click "Invite Members" to send invitations.
|
||||
</VAlert>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
|
||||
<VCard v-else-if="isLoading">
|
||||
@@ -264,13 +140,6 @@ function getRsvpColor(status: string): string {
|
||||
/>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
|
||||
<!-- Invite Members Dialog -->
|
||||
<InviteMembersDialog
|
||||
v-model:is-dialog-open="isInviteDialogOpen"
|
||||
:event-id="eventId"
|
||||
@invited="fetchEvent(eventId)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { useEvents } from '@/composables/useEvents'
|
||||
import { useRouter } from 'vue-router'
|
||||
import type { CreateEventData } from '@/types/events'
|
||||
import type { CreateEventData, EventCrewEventStatus } from '@/types/events'
|
||||
|
||||
definePage({
|
||||
meta: {
|
||||
@@ -10,115 +10,68 @@ definePage({
|
||||
})
|
||||
|
||||
const router = useRouter()
|
||||
const { createEvent, isLoading } = useEvents()
|
||||
const { createEvent, isLoading, organisationId } = useEvents()
|
||||
|
||||
const formData = ref<CreateEventData>({
|
||||
title: '',
|
||||
description: '',
|
||||
event_date: '',
|
||||
start_time: '',
|
||||
end_time: '',
|
||||
load_in_time: '',
|
||||
soundcheck_time: '',
|
||||
location_id: '',
|
||||
customer_id: '',
|
||||
setlist_id: '',
|
||||
fee: undefined,
|
||||
currency: 'EUR',
|
||||
name: '',
|
||||
slug: '',
|
||||
start_date: '',
|
||||
end_date: '',
|
||||
timezone: 'Europe/Amsterdam',
|
||||
status: 'draft',
|
||||
visibility: 'members',
|
||||
rsvp_deadline: '',
|
||||
notes: '',
|
||||
internal_notes: '',
|
||||
is_public_setlist: false,
|
||||
})
|
||||
|
||||
// Separate date/time values for the pickers (format: "YYYY-MM-DD HH:mm")
|
||||
const startDateTime = ref('')
|
||||
const endDateTime = ref('')
|
||||
|
||||
// Watch startDateTime and sync to formData + auto-populate endDateTime
|
||||
watch(startDateTime, (newValue) => {
|
||||
if (newValue) {
|
||||
// Parse "YYYY-MM-DD HH:mm" format
|
||||
const parts = newValue.split(' ')
|
||||
if (parts.length === 2) {
|
||||
formData.value.event_date = parts[0] // YYYY-MM-DD
|
||||
formData.value.start_time = parts[1] // HH:mm
|
||||
}
|
||||
|
||||
// Auto-populate end date/time if empty
|
||||
if (!endDateTime.value) {
|
||||
endDateTime.value = newValue
|
||||
}
|
||||
// If end date is before start date, update it to match start
|
||||
else if (new Date(endDateTime.value) < new Date(newValue)) {
|
||||
endDateTime.value = newValue
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Watch endDateTime and sync to formData
|
||||
watch(endDateTime, (newValue) => {
|
||||
if (newValue) {
|
||||
const parts = newValue.split(' ')
|
||||
if (parts.length === 2) {
|
||||
formData.value.end_time = parts[1] // HH:mm
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Computed minDate for end date picker (can't be before start date)
|
||||
const endDateMinDate = computed(() => {
|
||||
if (startDateTime.value) {
|
||||
return startDateTime.value.split(' ')[0] // Return just the date part
|
||||
}
|
||||
return undefined
|
||||
})
|
||||
|
||||
const statusOptions = [
|
||||
const statusOptions: { title: string; value: EventCrewEventStatus }[] = [
|
||||
{ title: 'Draft', value: 'draft' },
|
||||
{ title: 'Pending', value: 'pending' },
|
||||
{ title: 'Confirmed', value: 'confirmed' },
|
||||
]
|
||||
|
||||
const visibilityOptions = [
|
||||
{ title: 'Private', value: 'private' },
|
||||
{ title: 'Members', value: 'members' },
|
||||
{ title: 'Public', value: 'public' },
|
||||
{ title: 'Published', value: 'published' },
|
||||
{ title: 'Registration open', value: 'registration_open' },
|
||||
{ title: 'Build-up', value: 'buildup' },
|
||||
{ title: 'Show day', value: 'showday' },
|
||||
{ title: 'Teardown', value: 'teardown' },
|
||||
{ title: 'Closed', value: 'closed' },
|
||||
]
|
||||
|
||||
const errors = ref<Record<string, string>>({})
|
||||
|
||||
function flattenValidationErrors(raw: Record<string, string[] | string>): Record<string, string> {
|
||||
const out: Record<string, string> = {}
|
||||
for (const [key, val] of Object.entries(raw)) {
|
||||
out[key] = Array.isArray(val) ? val[0]! : val
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
errors.value = {}
|
||||
|
||||
// Basic validation
|
||||
if (!formData.value.title) {
|
||||
errors.value.title = 'Title is required'
|
||||
if (!formData.value.name) {
|
||||
errors.value.name = 'Name is required'
|
||||
return
|
||||
}
|
||||
if (!startDateTime.value) {
|
||||
errors.value.event_date = 'Start date and time is required'
|
||||
if (!formData.value.slug) {
|
||||
errors.value.slug = 'Slug is required'
|
||||
return
|
||||
}
|
||||
if (!formData.value.event_date) {
|
||||
errors.value.event_date = 'Event date is required'
|
||||
if (!formData.value.start_date) {
|
||||
errors.value.start_date = 'Start date is required'
|
||||
return
|
||||
}
|
||||
if (!formData.value.start_time) {
|
||||
errors.value.start_time = 'Start time is required'
|
||||
if (!formData.value.end_date) {
|
||||
errors.value.end_date = 'End date is required'
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const event = await createEvent(formData.value)
|
||||
router.push(`/events/${event.id}`)
|
||||
} catch (err: any) {
|
||||
if (err.response?.data?.errors) {
|
||||
errors.value = err.response.data.errors
|
||||
} else {
|
||||
errors.value._general = err.message || 'Failed to create event'
|
||||
await router.push(`/events/${event.id}`)
|
||||
}
|
||||
catch (err: unknown) {
|
||||
const ax = err as { response?: { data?: { errors?: Record<string, string[] | string>; message?: string } } }
|
||||
if (ax.response?.data?.errors) {
|
||||
errors.value = flattenValidationErrors(ax.response.data.errors)
|
||||
}
|
||||
else {
|
||||
errors.value._general = ax.response?.data?.message ?? (err instanceof Error ? err.message : 'Failed to create event')
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -126,10 +79,21 @@ async function handleSubmit() {
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<VAlert
|
||||
v-if="!organisationId"
|
||||
type="warning"
|
||||
class="mb-4"
|
||||
>
|
||||
You need an organisation before creating events. Log in with a user that belongs to an organisation.
|
||||
</VAlert>
|
||||
|
||||
<VCard>
|
||||
<VCardTitle class="d-flex align-center gap-2">
|
||||
<VIcon icon="tabler-arrow-left" />
|
||||
<RouterLink to="/events" class="text-decoration-none">
|
||||
<RouterLink
|
||||
to="/events"
|
||||
class="text-decoration-none"
|
||||
>
|
||||
Back to Events
|
||||
</RouterLink>
|
||||
</VCardTitle>
|
||||
@@ -140,88 +104,93 @@ async function handleSubmit() {
|
||||
<VForm @submit.prevent="handleSubmit">
|
||||
<VRow>
|
||||
<VCol cols="12">
|
||||
<AppTextField v-model="formData.title" label="Title" placeholder="Event Title"
|
||||
:error-messages="errors.title" required />
|
||||
<AppTextField
|
||||
v-model="formData.name"
|
||||
label="Name"
|
||||
placeholder="Summer Festival 2026"
|
||||
:error-messages="errors.name"
|
||||
required
|
||||
/>
|
||||
</VCol>
|
||||
|
||||
<VCol cols="12">
|
||||
<AppTextarea v-model="formData.description" label="Description" placeholder="Event Description"
|
||||
rows="3" />
|
||||
<AppTextField
|
||||
v-model="formData.slug"
|
||||
label="Slug"
|
||||
placeholder="summer-festival-2026"
|
||||
hint="Lowercase letters, numbers, and hyphens only"
|
||||
:error-messages="errors.slug"
|
||||
required
|
||||
/>
|
||||
</VCol>
|
||||
|
||||
<VCol cols="12" md="6">
|
||||
<AppDateTimePicker v-model="startDateTime" label="Start Date & Time"
|
||||
placeholder="Select start date and time" :error-messages="errors.event_date || errors.start_time"
|
||||
:config="{
|
||||
enableTime: true,
|
||||
time_24hr: true,
|
||||
dateFormat: 'Y-m-d H:i',
|
||||
minDate: 'today',
|
||||
}" />
|
||||
<VCol
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<AppTextField
|
||||
v-model="formData.start_date"
|
||||
label="Start date"
|
||||
type="date"
|
||||
:error-messages="errors.start_date"
|
||||
required
|
||||
/>
|
||||
</VCol>
|
||||
|
||||
<VCol cols="12" md="6">
|
||||
<AppDateTimePicker v-model="endDateTime" label="End Date & Time" placeholder="Select end date and time"
|
||||
:error-messages="errors.end_time" :config="{
|
||||
enableTime: true,
|
||||
time_24hr: true,
|
||||
dateFormat: 'Y-m-d H:i',
|
||||
minDate: endDateMinDate,
|
||||
}" />
|
||||
</VCol>
|
||||
|
||||
<VCol cols="12" md="6">
|
||||
<AppTextField v-model="formData.load_in_time" label="Load-in Time" type="time" />
|
||||
</VCol>
|
||||
|
||||
<VCol cols="12" md="6">
|
||||
<AppTextField v-model="formData.soundcheck_time" label="Soundcheck Time" type="time" />
|
||||
</VCol>
|
||||
|
||||
<VCol cols="12" md="6">
|
||||
<AppTextField v-model="formData.rsvp_deadline" label="RSVP Deadline" type="datetime-local" />
|
||||
</VCol>
|
||||
|
||||
<VCol cols="12" md="6">
|
||||
<AppTextField v-model.number="formData.fee" label="Fee" type="number" step="0.01" prefix="€" />
|
||||
</VCol>
|
||||
|
||||
<VCol cols="12" md="6">
|
||||
<AppSelect v-model="formData.currency" label="Currency"
|
||||
:items="[{ title: 'EUR', value: 'EUR' }, { title: 'USD', value: 'USD' }]" />
|
||||
</VCol>
|
||||
|
||||
<VCol cols="12" md="6">
|
||||
<AppSelect v-model="formData.status" label="Status" :items="statusOptions" />
|
||||
</VCol>
|
||||
|
||||
<VCol cols="12" md="6">
|
||||
<AppSelect v-model="formData.visibility" label="Visibility" :items="visibilityOptions" />
|
||||
<VCol
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<AppTextField
|
||||
v-model="formData.end_date"
|
||||
label="End date"
|
||||
type="date"
|
||||
:error-messages="errors.end_date"
|
||||
required
|
||||
/>
|
||||
</VCol>
|
||||
|
||||
<VCol cols="12">
|
||||
<AppTextarea v-model="formData.notes" label="Notes" placeholder="Public notes" rows="3" />
|
||||
<AppTextField
|
||||
v-model="formData.timezone"
|
||||
label="Timezone"
|
||||
placeholder="Europe/Amsterdam"
|
||||
:error-messages="errors.timezone"
|
||||
/>
|
||||
</VCol>
|
||||
|
||||
<VCol cols="12">
|
||||
<AppTextarea v-model="formData.internal_notes" label="Internal Notes"
|
||||
placeholder="Private notes (only visible to admins)" rows="3" />
|
||||
<AppSelect
|
||||
v-model="formData.status"
|
||||
label="Status"
|
||||
:items="statusOptions"
|
||||
:error-messages="errors.status"
|
||||
/>
|
||||
</VCol>
|
||||
|
||||
<VCol cols="12">
|
||||
<VCheckbox v-model="formData.is_public_setlist" label="Public Setlist" />
|
||||
</VCol>
|
||||
|
||||
<VCol cols="12">
|
||||
<VAlert v-if="errors._general" type="error" class="mb-4">
|
||||
<VAlert
|
||||
v-if="errors._general"
|
||||
type="error"
|
||||
class="mb-4"
|
||||
>
|
||||
{{ errors._general }}
|
||||
</VAlert>
|
||||
|
||||
<div class="d-flex gap-4">
|
||||
<VBtn type="submit" color="primary" :loading="isLoading">
|
||||
<VBtn
|
||||
type="submit"
|
||||
color="primary"
|
||||
:loading="isLoading"
|
||||
:disabled="!organisationId"
|
||||
>
|
||||
Create Event
|
||||
</VBtn>
|
||||
<VBtn variant="tonal" color="secondary" to="/events">
|
||||
<VBtn
|
||||
variant="tonal"
|
||||
color="secondary"
|
||||
to="/events"
|
||||
>
|
||||
Cancel
|
||||
</VBtn>
|
||||
</div>
|
||||
|
||||
@@ -1,94 +1,82 @@
|
||||
<script setup lang="ts">
|
||||
import { useEvents } from '@/composables/useEvents'
|
||||
import type { Event } from '@/types/events'
|
||||
|
||||
definePage({
|
||||
meta: {
|
||||
navActiveLink: 'events',
|
||||
},
|
||||
})
|
||||
|
||||
const { events, pagination, isLoading, error, fetchEvents, deleteEvent } = useEvents()
|
||||
const { events, pagination, organisationId, isLoading, error, fetchEvents } = useEvents()
|
||||
|
||||
const searchQuery = ref('')
|
||||
const selectedStatus = ref<string | undefined>()
|
||||
const selectedRows = ref([])
|
||||
|
||||
// Data table options
|
||||
const itemsPerPage = ref(10)
|
||||
const page = ref(1)
|
||||
|
||||
const statusOptions = [
|
||||
{ title: 'Draft', value: 'draft' },
|
||||
{ title: 'Pending', value: 'pending' },
|
||||
{ title: 'Confirmed', value: 'confirmed' },
|
||||
{ title: 'Completed', value: 'completed' },
|
||||
{ title: 'Cancelled', value: 'cancelled' },
|
||||
]
|
||||
|
||||
const headers = [
|
||||
{ title: 'Title', key: 'title' },
|
||||
{ title: 'Date', key: 'event_date' },
|
||||
{ title: 'Time', key: 'start_time' },
|
||||
{ title: 'Location', key: 'location' },
|
||||
{ title: 'Name', key: 'name' },
|
||||
{ title: 'Start', key: 'start_date' },
|
||||
{ title: 'End', key: 'end_date' },
|
||||
{ title: 'Timezone', key: 'timezone' },
|
||||
{ title: 'Status', key: 'status' },
|
||||
{ title: 'Actions', key: 'actions', sortable: false },
|
||||
]
|
||||
|
||||
const isDeleteDialogOpen = ref(false)
|
||||
const eventToDelete = ref<Event | null>(null)
|
||||
|
||||
// Fetch events on mount and when filters change
|
||||
watch([page, itemsPerPage, selectedStatus, searchQuery], async () => {
|
||||
watch([page, itemsPerPage], async () => {
|
||||
if (!organisationId.value) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
await fetchEvents({
|
||||
page: page.value,
|
||||
per_page: itemsPerPage.value,
|
||||
status: selectedStatus.value,
|
||||
})
|
||||
} catch (err) {
|
||||
// Error is already handled in the composable
|
||||
}
|
||||
catch (err) {
|
||||
console.error('Failed to fetch events:', err)
|
||||
}
|
||||
}, { immediate: true })
|
||||
|
||||
function handleDelete(event: Event) {
|
||||
eventToDelete.value = event
|
||||
isDeleteDialogOpen.value = true
|
||||
}
|
||||
|
||||
async function confirmDelete() {
|
||||
if (eventToDelete.value) {
|
||||
watch(organisationId, async (id) => {
|
||||
if (id) {
|
||||
try {
|
||||
await deleteEvent(eventToDelete.value.id)
|
||||
isDeleteDialogOpen.value = false
|
||||
eventToDelete.value = null
|
||||
// Refresh list
|
||||
await fetchEvents({
|
||||
page: page.value,
|
||||
per_page: itemsPerPage.value,
|
||||
status: selectedStatus.value,
|
||||
})
|
||||
} catch (err) {
|
||||
console.error('Failed to delete event:', err)
|
||||
}
|
||||
catch (err) {
|
||||
console.error('Failed to fetch events:', err)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
function getStatusColor(status: string): string {
|
||||
const colors: Record<string, string> = {
|
||||
draft: 'secondary',
|
||||
pending: 'warning',
|
||||
confirmed: 'success',
|
||||
completed: 'info',
|
||||
cancelled: 'error',
|
||||
published: 'info',
|
||||
registration_open: 'success',
|
||||
buildup: 'warning',
|
||||
showday: 'primary',
|
||||
teardown: 'warning',
|
||||
closed: 'secondary',
|
||||
}
|
||||
return colors[status] || 'secondary'
|
||||
}
|
||||
|
||||
function formatStatusLabel(status: string): string {
|
||||
return status.replaceAll('_', ' ')
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<VAlert
|
||||
v-if="!organisationId"
|
||||
type="warning"
|
||||
class="mb-4"
|
||||
>
|
||||
You need an organisation to manage events. Create an organisation in the API or attach your user to one, then log in again.
|
||||
</VAlert>
|
||||
|
||||
<VCard>
|
||||
<VCardText class="d-flex justify-space-between align-center flex-wrap gap-4">
|
||||
<div class="d-flex gap-4 align-center flex-wrap">
|
||||
@@ -104,38 +92,26 @@ function getStatusColor(status: string): string {
|
||||
color="primary"
|
||||
prepend-icon="tabler-plus"
|
||||
to="/events/create"
|
||||
:disabled="!organisationId"
|
||||
>
|
||||
Create Event
|
||||
</VBtn>
|
||||
</div>
|
||||
|
||||
<div class="d-flex align-center flex-wrap gap-4">
|
||||
<AppTextField
|
||||
v-model="searchQuery"
|
||||
placeholder="Search Events"
|
||||
style="inline-size: 200px;"
|
||||
/>
|
||||
<AppSelect
|
||||
v-model="selectedStatus"
|
||||
placeholder="Filter by Status"
|
||||
clearable
|
||||
:items="statusOptions"
|
||||
style="inline-size: 180px;"
|
||||
/>
|
||||
</div>
|
||||
</VCardText>
|
||||
|
||||
<VDivider />
|
||||
|
||||
<!-- Loading State -->
|
||||
<div v-if="isLoading" class="d-flex justify-center align-center py-12">
|
||||
<div
|
||||
v-if="isLoading"
|
||||
class="d-flex justify-center align-center py-12"
|
||||
>
|
||||
<VProgressCircular
|
||||
indeterminate
|
||||
color="primary"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Error State -->
|
||||
<VAlert
|
||||
v-else-if="error"
|
||||
type="error"
|
||||
@@ -144,36 +120,30 @@ function getStatusColor(status: string): string {
|
||||
{{ error.message }}
|
||||
</VAlert>
|
||||
|
||||
<!-- Events Table -->
|
||||
<VDataTable
|
||||
v-else
|
||||
v-model:items-per-page="itemsPerPage"
|
||||
v-model:model-value="selectedRows"
|
||||
v-model:page="page"
|
||||
:headers="headers"
|
||||
:items="events"
|
||||
:items-length="pagination?.total || 0"
|
||||
:items-length="pagination?.total || events.length"
|
||||
class="text-no-wrap"
|
||||
>
|
||||
<template #item.title="{ item }">
|
||||
<template #item.name="{ item }">
|
||||
<RouterLink
|
||||
:to="`/events/${item.id}`"
|
||||
class="text-high-emphasis text-decoration-none"
|
||||
>
|
||||
{{ item.title }}
|
||||
{{ item.name }}
|
||||
</RouterLink>
|
||||
</template>
|
||||
|
||||
<template #item.event_date="{ item }">
|
||||
{{ new Date(item.event_date).toLocaleDateString() }}
|
||||
<template #item.start_date="{ item }">
|
||||
{{ new Date(item.start_date).toLocaleDateString() }}
|
||||
</template>
|
||||
|
||||
<template #item.start_time="{ item }">
|
||||
{{ item.start_time }}
|
||||
</template>
|
||||
|
||||
<template #item.location="{ item }">
|
||||
{{ item.location?.name || '-' }}
|
||||
<template #item.end_date="{ item }">
|
||||
{{ new Date(item.end_date).toLocaleDateString() }}
|
||||
</template>
|
||||
|
||||
<template #item.status="{ item }">
|
||||
@@ -181,28 +151,17 @@ function getStatusColor(status: string): string {
|
||||
:color="getStatusColor(item.status)"
|
||||
size="small"
|
||||
>
|
||||
{{ item.status_label }}
|
||||
{{ formatStatusLabel(item.status) }}
|
||||
</VChip>
|
||||
</template>
|
||||
|
||||
<template #item.actions="{ item }">
|
||||
<IconBtn
|
||||
:to="`/events/${item.id}`"
|
||||
>
|
||||
<IconBtn :to="`/events/${item.id}`">
|
||||
<VIcon icon="tabler-eye" />
|
||||
</IconBtn>
|
||||
|
||||
<IconBtn
|
||||
:to="`/events/${item.id}/edit`"
|
||||
>
|
||||
<IconBtn :to="`/events/${item.id}/edit`">
|
||||
<VIcon icon="tabler-pencil" />
|
||||
</IconBtn>
|
||||
|
||||
<IconBtn
|
||||
@click="handleDelete(item)"
|
||||
>
|
||||
<VIcon icon="tabler-trash" />
|
||||
</IconBtn>
|
||||
</template>
|
||||
|
||||
<template #bottom>
|
||||
@@ -216,34 +175,5 @@ function getStatusColor(status: string): string {
|
||||
</template>
|
||||
</VDataTable>
|
||||
</VCard>
|
||||
|
||||
<!-- Delete Confirmation Dialog -->
|
||||
<VDialog
|
||||
v-model="isDeleteDialogOpen"
|
||||
max-width="400"
|
||||
>
|
||||
<VCard>
|
||||
<VCardTitle>Delete Event?</VCardTitle>
|
||||
<VCardText>
|
||||
Are you sure you want to delete "{{ eventToDelete?.title }}"? This action cannot be undone.
|
||||
</VCardText>
|
||||
<VCardActions>
|
||||
<VSpacer />
|
||||
<VBtn
|
||||
variant="text"
|
||||
@click="isDeleteDialogOpen = false"
|
||||
>
|
||||
Cancel
|
||||
</VBtn>
|
||||
<VBtn
|
||||
color="error"
|
||||
@click="confirmDelete"
|
||||
>
|
||||
Delete
|
||||
</VBtn>
|
||||
</VCardActions>
|
||||
</VCard>
|
||||
</VDialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -11,6 +11,18 @@ import authV2MaskDark from '@images/pages/misc-mask-dark.png'
|
||||
import authV2MaskLight from '@images/pages/misc-mask-light.png'
|
||||
import { VNodeRenderer } from '@layouts/components/VNodeRenderer'
|
||||
import { themeConfig } from '@themeConfig'
|
||||
import { getUserAbilityRules } from '@/utils/auth-ability'
|
||||
import type { Rule } from '@/plugins/casl/ability'
|
||||
import type { AuthUserCookie } from '@/composables/useOrganisationContext'
|
||||
|
||||
interface LoginApiPayload {
|
||||
success: boolean
|
||||
data: {
|
||||
user: AuthUserCookie & Record<string, unknown>
|
||||
token: string
|
||||
}
|
||||
message?: string
|
||||
}
|
||||
|
||||
const authThemeImg = useGenerateImageVariant(authV2LoginIllustrationLight, authV2LoginIllustrationDark, authV2LoginIllustrationBorderedLight, authV2LoginIllustrationBorderedDark, true)
|
||||
|
||||
@@ -46,7 +58,7 @@ const rememberMe = ref(false)
|
||||
|
||||
const login = async () => {
|
||||
try {
|
||||
const res = await $api('/auth/login', {
|
||||
const res = await $api<LoginApiPayload>('/auth/login', {
|
||||
method: 'POST',
|
||||
body: {
|
||||
email: credentials.value.email,
|
||||
@@ -71,14 +83,14 @@ const login = async () => {
|
||||
const userData = data.user
|
||||
const accessToken = data.token
|
||||
|
||||
// Set ability rules based on user role
|
||||
const userAbilityRules = getUserAbilityRules(userData.role)
|
||||
const roles = Array.isArray(userData.roles) ? userData.roles : []
|
||||
const userAbilityRules = getUserAbilityRules(roles)
|
||||
|
||||
useCookie('userAbilityRules').value = userAbilityRules
|
||||
useCookie<Rule[]>('userAbilityRules').value = userAbilityRules
|
||||
ability.update(userAbilityRules)
|
||||
|
||||
useCookie('userData').value = userData
|
||||
useCookie('accessToken').value = accessToken
|
||||
useCookie<AuthUserCookie>('userData').value = userData
|
||||
useCookie<string>('accessToken').value = accessToken
|
||||
|
||||
// Redirect to `to` query if exist or redirect to index route
|
||||
await nextTick()
|
||||
@@ -89,42 +101,6 @@ const login = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
// Generate ability rules based on user role
|
||||
function getUserAbilityRules(role: string | null) {
|
||||
// Admin can do everything
|
||||
if (role === 'admin') {
|
||||
return [{ action: 'manage', subject: 'all' }]
|
||||
}
|
||||
|
||||
// Booking agent can manage events and customers
|
||||
if (role === 'booking_agent') {
|
||||
return [
|
||||
{ action: 'read', subject: 'all' },
|
||||
{ action: 'manage', subject: 'Event' },
|
||||
{ action: 'manage', subject: 'Customer' },
|
||||
{ action: 'manage', subject: 'Location' },
|
||||
{ action: 'manage', subject: 'BookingRequest' },
|
||||
]
|
||||
}
|
||||
|
||||
// Music manager can manage music and setlists
|
||||
if (role === 'music_manager') {
|
||||
return [
|
||||
{ action: 'read', subject: 'all' },
|
||||
{ action: 'manage', subject: 'MusicNumber' },
|
||||
{ action: 'manage', subject: 'Setlist' },
|
||||
]
|
||||
}
|
||||
|
||||
// Default member permissions
|
||||
return [
|
||||
{ action: 'read', subject: 'Event' },
|
||||
{ action: 'read', subject: 'MusicNumber' },
|
||||
{ action: 'read', subject: 'Setlist' },
|
||||
{ action: 'manage', subject: 'User', conditions: { id: '{{ user.id }}' } },
|
||||
]
|
||||
}
|
||||
|
||||
const onSubmit = () => {
|
||||
refVForm.value?.validate()
|
||||
.then(({ valid: isValid }) => {
|
||||
|
||||
@@ -4,6 +4,18 @@ import { VForm } from 'vuetify/components/VForm'
|
||||
import AuthProvider from '@/views/pages/authentication/AuthProvider.vue'
|
||||
import { VNodeRenderer } from '@layouts/components/VNodeRenderer'
|
||||
import { themeConfig } from '@themeConfig'
|
||||
import { getUserAbilityRules } from '@/utils/auth-ability'
|
||||
import type { Rule } from '@/plugins/casl/ability'
|
||||
import type { AuthUserCookie } from '@/composables/useOrganisationContext'
|
||||
|
||||
interface RegisterApiPayload {
|
||||
success: boolean
|
||||
data: {
|
||||
user: AuthUserCookie & Record<string, unknown>
|
||||
token: string
|
||||
}
|
||||
message?: string
|
||||
}
|
||||
|
||||
import authV2RegisterIllustrationBorderedDark from '@images/pages/auth-v2-register-illustration-bordered-dark.png'
|
||||
import authV2RegisterIllustrationBorderedLight from '@images/pages/auth-v2-register-illustration-bordered-light.png'
|
||||
@@ -50,7 +62,7 @@ const refVForm = ref<VForm>()
|
||||
const register = async () => {
|
||||
isLoading.value = true
|
||||
try {
|
||||
const res = await $api('/auth/register', {
|
||||
const res = await $api<RegisterApiPayload>('/auth/register', {
|
||||
method: 'POST',
|
||||
body: {
|
||||
name: form.value.name,
|
||||
@@ -77,18 +89,13 @@ const register = async () => {
|
||||
const userData = data.user
|
||||
const accessToken = data.token
|
||||
|
||||
// Set ability rules based on user role
|
||||
const userAbilityRules = [
|
||||
{ action: 'read', subject: 'Event' },
|
||||
{ action: 'read', subject: 'MusicNumber' },
|
||||
{ action: 'read', subject: 'Setlist' },
|
||||
{ action: 'manage', subject: 'User', conditions: { id: userData.id } },
|
||||
]
|
||||
const roles = Array.isArray(userData.roles) ? userData.roles : []
|
||||
const userAbilityRules = getUserAbilityRules(roles)
|
||||
|
||||
useCookie('userAbilityRules').value = userAbilityRules
|
||||
useCookie<Rule[]>('userAbilityRules').value = userAbilityRules
|
||||
ability.update(userAbilityRules)
|
||||
useCookie('userData').value = userData
|
||||
useCookie('accessToken').value = accessToken
|
||||
useCookie<AuthUserCookie>('userData').value = userData
|
||||
useCookie<string>('accessToken').value = accessToken
|
||||
|
||||
await nextTick(() => {
|
||||
router.replace('/')
|
||||
|
||||
Reference in New Issue
Block a user