feat(app): auth, orgs/events UI, router guards, and dev tooling
- Add Sanctum auth flow (store, composables, login, axios interceptors) - Add dashboard, organisation list/detail, events CRUD dialogs - Wire router guards, navigation, organisation switcher in layout - Replace Vuexy @db types in NavSearchBar; add @iconify/types; themeConfig title typing - Vuetify settings.scss + resolve configFile via fileURLToPath; drop dead path aliases - Root index redirects to dashboard; fix events table route name - API: DevSeeder + DatabaseSeeder updates; docs TEST_SCENARIO; corporate identity assets Made-with: Cursor
This commit is contained in:
179
apps/app/src/components/events/CreateEventDialog.vue
Normal file
179
apps/app/src/components/events/CreateEventDialog.vue
Normal file
@@ -0,0 +1,179 @@
|
||||
<script setup lang="ts">
|
||||
import { VForm } from 'vuetify/components/VForm'
|
||||
import { useCreateEvent } from '@/composables/api/useEvents'
|
||||
import { requiredValidator } from '@core/utils/validators'
|
||||
|
||||
const props = defineProps<{
|
||||
orgId: string
|
||||
}>()
|
||||
|
||||
const modelValue = defineModel<boolean>({ required: true })
|
||||
|
||||
const orgIdRef = computed(() => props.orgId)
|
||||
|
||||
const form = ref({
|
||||
name: '',
|
||||
slug: '',
|
||||
start_date: '',
|
||||
end_date: '',
|
||||
timezone: 'Europe/Amsterdam',
|
||||
})
|
||||
|
||||
const errors = ref<Record<string, string>>({})
|
||||
const refVForm = ref<VForm>()
|
||||
const showSuccess = ref(false)
|
||||
|
||||
const { mutate: createEvent, isPending } = useCreateEvent(orgIdRef)
|
||||
|
||||
const timezoneOptions = [
|
||||
{ title: 'Europe/Amsterdam', value: 'Europe/Amsterdam' },
|
||||
{ title: 'Europe/London', value: 'Europe/London' },
|
||||
{ title: 'Europe/Paris', value: 'Europe/Paris' },
|
||||
{ title: 'UTC', value: 'UTC' },
|
||||
]
|
||||
|
||||
watch(() => form.value.name, (name) => {
|
||||
form.value.slug = name
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9\s-]/g, '')
|
||||
.replace(/\s+/g, '-')
|
||||
.replace(/-+/g, '-')
|
||||
.replace(/^-|-$/g, '')
|
||||
})
|
||||
|
||||
const endDateRule = (v: string) => {
|
||||
if (!v) return 'Einddatum is verplicht'
|
||||
if (form.value.start_date && v < form.value.start_date) {
|
||||
return 'Einddatum moet op of na startdatum liggen'
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
function resetForm() {
|
||||
form.value = { name: '', slug: '', start_date: '', end_date: '', timezone: 'Europe/Amsterdam' }
|
||||
errors.value = {}
|
||||
refVForm.value?.resetValidation()
|
||||
}
|
||||
|
||||
function onSubmit() {
|
||||
refVForm.value?.validate().then(({ valid }) => {
|
||||
if (!valid) return
|
||||
|
||||
errors.value = {}
|
||||
|
||||
createEvent(form.value, {
|
||||
onSuccess: () => {
|
||||
modelValue.value = false
|
||||
showSuccess.value = true
|
||||
resetForm()
|
||||
},
|
||||
onError: (err: any) => {
|
||||
const data = err.response?.data
|
||||
if (data?.errors) {
|
||||
errors.value = Object.fromEntries(
|
||||
Object.entries(data.errors).map(([k, v]) => [k, (v as string[])[0]]),
|
||||
)
|
||||
}
|
||||
else if (data?.message) {
|
||||
errors.value = { name: data.message }
|
||||
}
|
||||
},
|
||||
})
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VDialog
|
||||
v-model="modelValue"
|
||||
max-width="550"
|
||||
@after-leave="resetForm"
|
||||
>
|
||||
<VCard title="Nieuw evenement">
|
||||
<VCardText>
|
||||
<VForm
|
||||
ref="refVForm"
|
||||
@submit.prevent="onSubmit"
|
||||
>
|
||||
<VRow>
|
||||
<VCol cols="12">
|
||||
<AppTextField
|
||||
v-model="form.name"
|
||||
label="Naam"
|
||||
:rules="[requiredValidator]"
|
||||
:error-messages="errors.name"
|
||||
autofocus
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12">
|
||||
<AppTextField
|
||||
v-model="form.slug"
|
||||
label="Slug"
|
||||
:rules="[requiredValidator]"
|
||||
:error-messages="errors.slug"
|
||||
hint="Wordt gebruikt in de URL"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
<VCol
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<AppTextField
|
||||
v-model="form.start_date"
|
||||
label="Startdatum"
|
||||
type="date"
|
||||
:rules="[requiredValidator]"
|
||||
:error-messages="errors.start_date"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<AppTextField
|
||||
v-model="form.end_date"
|
||||
label="Einddatum"
|
||||
type="date"
|
||||
:rules="[requiredValidator, endDateRule]"
|
||||
:error-messages="errors.end_date"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12">
|
||||
<AppSelect
|
||||
v-model="form.timezone"
|
||||
label="Tijdzone"
|
||||
:items="timezoneOptions"
|
||||
:error-messages="errors.timezone"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VForm>
|
||||
</VCardText>
|
||||
<VCardActions>
|
||||
<VSpacer />
|
||||
<VBtn
|
||||
variant="text"
|
||||
@click="modelValue = false"
|
||||
>
|
||||
Annuleren
|
||||
</VBtn>
|
||||
<VBtn
|
||||
color="primary"
|
||||
:loading="isPending"
|
||||
@click="onSubmit"
|
||||
>
|
||||
Aanmaken
|
||||
</VBtn>
|
||||
</VCardActions>
|
||||
</VCard>
|
||||
</VDialog>
|
||||
|
||||
<VSnackbar
|
||||
v-model="showSuccess"
|
||||
color="success"
|
||||
:timeout="3000"
|
||||
>
|
||||
Evenement aangemaakt
|
||||
</VSnackbar>
|
||||
</template>
|
||||
199
apps/app/src/components/events/EditEventDialog.vue
Normal file
199
apps/app/src/components/events/EditEventDialog.vue
Normal file
@@ -0,0 +1,199 @@
|
||||
<script setup lang="ts">
|
||||
import { VForm } from 'vuetify/components/VForm'
|
||||
import { useUpdateEvent } from '@/composables/api/useEvents'
|
||||
import { requiredValidator } from '@core/utils/validators'
|
||||
import type { EventStatus, EventType } from '@/types/event'
|
||||
|
||||
const props = defineProps<{
|
||||
event: EventType
|
||||
orgId: string
|
||||
}>()
|
||||
|
||||
const modelValue = defineModel<boolean>({ required: true })
|
||||
|
||||
const orgIdRef = computed(() => props.orgId)
|
||||
const eventIdRef = computed(() => props.event.id)
|
||||
|
||||
const form = ref({
|
||||
name: '',
|
||||
slug: '',
|
||||
start_date: '',
|
||||
end_date: '',
|
||||
timezone: '',
|
||||
status: '' as EventStatus,
|
||||
})
|
||||
|
||||
const errors = ref<Record<string, string>>({})
|
||||
const refVForm = ref<VForm>()
|
||||
const showSuccess = ref(false)
|
||||
|
||||
const { mutate: updateEvent, isPending } = useUpdateEvent(orgIdRef, eventIdRef)
|
||||
|
||||
const timezoneOptions = [
|
||||
{ title: 'Europe/Amsterdam', value: 'Europe/Amsterdam' },
|
||||
{ title: 'Europe/London', value: 'Europe/London' },
|
||||
{ title: 'Europe/Paris', value: 'Europe/Paris' },
|
||||
{ title: 'UTC', value: 'UTC' },
|
||||
]
|
||||
|
||||
const statusOptions: { title: string; value: EventStatus }[] = [
|
||||
{ title: 'Draft', value: 'draft' },
|
||||
{ title: 'Published', value: 'published' },
|
||||
{ title: 'Registration Open', value: 'registration_open' },
|
||||
{ title: 'Build-up', value: 'buildup' },
|
||||
{ title: 'Show Day', value: 'showday' },
|
||||
{ title: 'Tear-down', value: 'teardown' },
|
||||
{ title: 'Closed', value: 'closed' },
|
||||
]
|
||||
|
||||
const endDateRule = (v: string) => {
|
||||
if (!v) return 'Einddatum is verplicht'
|
||||
if (form.value.start_date && v < form.value.start_date) {
|
||||
return 'Einddatum moet op of na startdatum liggen'
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
watch(() => props.event, (ev) => {
|
||||
form.value = {
|
||||
name: ev.name,
|
||||
slug: ev.slug,
|
||||
start_date: ev.start_date,
|
||||
end_date: ev.end_date,
|
||||
timezone: ev.timezone,
|
||||
status: ev.status,
|
||||
}
|
||||
}, { immediate: true })
|
||||
|
||||
function onSubmit() {
|
||||
refVForm.value?.validate().then(({ valid }) => {
|
||||
if (!valid) return
|
||||
|
||||
errors.value = {}
|
||||
|
||||
updateEvent(form.value, {
|
||||
onSuccess: () => {
|
||||
modelValue.value = false
|
||||
showSuccess.value = true
|
||||
},
|
||||
onError: (err: any) => {
|
||||
const data = err.response?.data
|
||||
if (data?.errors) {
|
||||
errors.value = Object.fromEntries(
|
||||
Object.entries(data.errors).map(([k, v]) => [k, (v as string[])[0]]),
|
||||
)
|
||||
}
|
||||
else if (data?.message) {
|
||||
errors.value = { name: data.message }
|
||||
}
|
||||
},
|
||||
})
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VDialog
|
||||
v-model="modelValue"
|
||||
max-width="550"
|
||||
>
|
||||
<VCard title="Evenement bewerken">
|
||||
<VCardText>
|
||||
<VForm
|
||||
ref="refVForm"
|
||||
@submit.prevent="onSubmit"
|
||||
>
|
||||
<VRow>
|
||||
<VCol cols="12">
|
||||
<AppTextField
|
||||
v-model="form.name"
|
||||
label="Naam"
|
||||
:rules="[requiredValidator]"
|
||||
:error-messages="errors.name"
|
||||
autofocus
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12">
|
||||
<AppTextField
|
||||
v-model="form.slug"
|
||||
label="Slug"
|
||||
:rules="[requiredValidator]"
|
||||
:error-messages="errors.slug"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<AppTextField
|
||||
v-model="form.start_date"
|
||||
label="Startdatum"
|
||||
type="date"
|
||||
:rules="[requiredValidator]"
|
||||
:error-messages="errors.start_date"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<AppTextField
|
||||
v-model="form.end_date"
|
||||
label="Einddatum"
|
||||
type="date"
|
||||
:rules="[requiredValidator, endDateRule]"
|
||||
:error-messages="errors.end_date"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<AppSelect
|
||||
v-model="form.timezone"
|
||||
label="Tijdzone"
|
||||
:items="timezoneOptions"
|
||||
:error-messages="errors.timezone"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<AppSelect
|
||||
v-model="form.status"
|
||||
label="Status"
|
||||
:items="statusOptions"
|
||||
:error-messages="errors.status"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VForm>
|
||||
</VCardText>
|
||||
<VCardActions>
|
||||
<VSpacer />
|
||||
<VBtn
|
||||
variant="text"
|
||||
@click="modelValue = false"
|
||||
>
|
||||
Annuleren
|
||||
</VBtn>
|
||||
<VBtn
|
||||
color="primary"
|
||||
:loading="isPending"
|
||||
@click="onSubmit"
|
||||
>
|
||||
Opslaan
|
||||
</VBtn>
|
||||
</VCardActions>
|
||||
</VCard>
|
||||
</VDialog>
|
||||
|
||||
<VSnackbar
|
||||
v-model="showSuccess"
|
||||
color="success"
|
||||
:timeout="3000"
|
||||
>
|
||||
Evenement bijgewerkt
|
||||
</VSnackbar>
|
||||
</template>
|
||||
64
apps/app/src/components/layout/OrganisationSwitcher.vue
Normal file
64
apps/app/src/components/layout/OrganisationSwitcher.vue
Normal file
@@ -0,0 +1,64 @@
|
||||
<script setup lang="ts">
|
||||
import { useAuthStore } from '@/stores/useAuthStore'
|
||||
|
||||
const authStore = useAuthStore()
|
||||
|
||||
const hasMultipleOrgs = computed(() => authStore.organisations.length > 1)
|
||||
const currentOrgName = computed(() => authStore.currentOrganisation?.name ?? 'Geen organisatie')
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="organisation-switcher mx-4 mb-2">
|
||||
<!-- Single org: just show the name -->
|
||||
<div
|
||||
v-if="!hasMultipleOrgs"
|
||||
class="d-flex align-center gap-x-2 pa-2"
|
||||
>
|
||||
<VIcon
|
||||
icon="tabler-building"
|
||||
size="20"
|
||||
color="primary"
|
||||
/>
|
||||
<span class="text-body-2 font-weight-medium text-truncate">
|
||||
{{ currentOrgName }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Multiple orgs: dropdown -->
|
||||
<VBtn
|
||||
v-else
|
||||
variant="tonal"
|
||||
color="primary"
|
||||
block
|
||||
class="text-none justify-start"
|
||||
prepend-icon="tabler-building"
|
||||
>
|
||||
<span class="text-truncate">{{ currentOrgName }}</span>
|
||||
|
||||
<VMenu
|
||||
activator="parent"
|
||||
width="250"
|
||||
location="bottom start"
|
||||
>
|
||||
<VList density="compact">
|
||||
<VListItem
|
||||
v-for="org in authStore.organisations"
|
||||
:key="org.id"
|
||||
:active="org.id === authStore.currentOrganisation?.id"
|
||||
@click="authStore.setActiveOrganisation(org.id)"
|
||||
>
|
||||
<VListItemTitle>{{ org.name }}</VListItemTitle>
|
||||
<template #append>
|
||||
<VChip
|
||||
size="x-small"
|
||||
variant="text"
|
||||
>
|
||||
{{ org.role }}
|
||||
</VChip>
|
||||
</template>
|
||||
</VListItem>
|
||||
</VList>
|
||||
</VMenu>
|
||||
</VBtn>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,98 @@
|
||||
<script setup lang="ts">
|
||||
import { VForm } from 'vuetify/components/VForm'
|
||||
import { useUpdateOrganisation } from '@/composables/api/useOrganisations'
|
||||
import { requiredValidator } from '@core/utils/validators'
|
||||
import type { Organisation } from '@/types/organisation'
|
||||
|
||||
const props = defineProps<{
|
||||
organisation: Organisation
|
||||
}>()
|
||||
|
||||
const modelValue = defineModel<boolean>({ required: true })
|
||||
|
||||
const name = ref('')
|
||||
const errors = ref<Record<string, string>>({})
|
||||
const refVForm = ref<VForm>()
|
||||
const showSuccess = ref(false)
|
||||
|
||||
const { mutate: updateOrganisation, isPending } = useUpdateOrganisation()
|
||||
|
||||
watch(() => props.organisation, (org) => {
|
||||
name.value = org.name
|
||||
}, { immediate: true })
|
||||
|
||||
function onSubmit() {
|
||||
refVForm.value?.validate().then(({ valid }) => {
|
||||
if (!valid) return
|
||||
|
||||
errors.value = {}
|
||||
|
||||
updateOrganisation(
|
||||
{ id: props.organisation.id, name: name.value },
|
||||
{
|
||||
onSuccess: () => {
|
||||
modelValue.value = false
|
||||
showSuccess.value = true
|
||||
},
|
||||
onError: (err: any) => {
|
||||
const data = err.response?.data
|
||||
if (data?.errors) {
|
||||
errors.value = { name: data.errors.name?.[0] ?? '' }
|
||||
}
|
||||
else if (data?.message) {
|
||||
errors.value = { name: data.message }
|
||||
}
|
||||
},
|
||||
},
|
||||
)
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VDialog
|
||||
v-model="modelValue"
|
||||
max-width="450"
|
||||
>
|
||||
<VCard title="Naam bewerken">
|
||||
<VCardText>
|
||||
<VForm
|
||||
ref="refVForm"
|
||||
@submit.prevent="onSubmit"
|
||||
>
|
||||
<AppTextField
|
||||
v-model="name"
|
||||
label="Organisatienaam"
|
||||
:rules="[requiredValidator]"
|
||||
:error-messages="errors.name"
|
||||
autofocus
|
||||
/>
|
||||
</VForm>
|
||||
</VCardText>
|
||||
<VCardActions>
|
||||
<VSpacer />
|
||||
<VBtn
|
||||
variant="text"
|
||||
@click="modelValue = false"
|
||||
>
|
||||
Annuleren
|
||||
</VBtn>
|
||||
<VBtn
|
||||
color="primary"
|
||||
:loading="isPending"
|
||||
@click="onSubmit"
|
||||
>
|
||||
Opslaan
|
||||
</VBtn>
|
||||
</VCardActions>
|
||||
</VCard>
|
||||
</VDialog>
|
||||
|
||||
<VSnackbar
|
||||
v-model="showSuccess"
|
||||
color="success"
|
||||
:timeout="3000"
|
||||
>
|
||||
Naam bijgewerkt
|
||||
</VSnackbar>
|
||||
</template>
|
||||
Reference in New Issue
Block a user