Files
crewli/apps/app/src/pages/platform/organisations/index.vue
bert.hausmans 2933d957a6 feat: add create organisation button and dialog on platform page
Add "Nieuwe organisatie" button to the platform organisations list page.
Dialog with name field (auto-generates slug) and slug field. Uses the
existing POST /organisations endpoint. On success, navigates to the
new organisation's detail page.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 01:27:40 +02:00

276 lines
7.4 KiB
Vue

<script setup lang="ts">
import { useAdminOrganisations, useCreateOrganisation } from '@/composables/api/useAdmin'
import type { AdminOrganisation, BillingStatus, CreateOrganisationPayload } from '@/types/admin'
definePage({
meta: {
navActiveLink: 'platform-organisations',
},
})
const router = useRouter()
const search = ref('')
const searchDebounced = refDebounced(search, 400)
const billingStatusFilter = ref<string>('')
const page = ref(1)
const itemsPerPage = ref(15)
const sortBy = ref('name')
const sortDirection = ref<'asc' | 'desc'>('asc')
const params = computed(() => ({
page: page.value,
per_page: itemsPerPage.value,
search: searchDebounced.value || undefined,
billing_status: billingStatusFilter.value || undefined,
sort: sortBy.value,
direction: sortDirection.value,
}))
const { data, isLoading, isError, refetch } = useAdminOrganisations(params)
const organisations = computed(() => data.value?.data ?? [])
const totalItems = computed(() => data.value?.meta?.total ?? 0)
const billingStatusColor: Record<BillingStatus, string> = {
trial: 'info',
active: 'success',
suspended: 'warning',
cancelled: 'error',
}
const billingStatusOptions = [
{ title: 'Alle statussen', value: '' },
{ title: 'Trial', value: 'trial' },
{ title: 'Active', value: 'active' },
{ title: 'Suspended', value: 'suspended' },
{ title: 'Cancelled', value: 'cancelled' },
]
const headers = [
{ title: 'Naam', key: 'name', sortable: true },
{ title: 'Slug', key: 'slug', sortable: false },
{ title: 'Status', key: 'billing_status', sortable: false },
{ title: 'Events', key: 'events_count', sortable: false, align: 'center' as const },
{ title: 'Gebruikers', key: 'users_count', sortable: false, align: 'center' as const },
{ title: 'Aangemaakt', key: 'created_at', sortable: true },
]
function formatDate(iso: string): string {
return new Date(iso).toLocaleDateString('nl-NL', {
day: '2-digit',
month: '2-digit',
year: 'numeric',
})
}
function onRowClick(_event: Event, { item }: { item: AdminOrganisation }) {
router.push({ name: 'platform-organisations-id', params: { id: item.id } })
}
function onUpdateOptions(options: { page: number; itemsPerPage: number; sortBy: Array<{ key: string; order: string }> }) {
page.value = options.page
itemsPerPage.value = options.itemsPerPage
if (options.sortBy.length > 0) {
sortBy.value = options.sortBy[0].key
sortDirection.value = options.sortBy[0].order as 'asc' | 'desc'
}
}
// ─── Create organisation ────────────────────────────────────
const isCreateDialogOpen = ref(false)
const createForm = ref<CreateOrganisationPayload>({ name: '', slug: '' })
const createError = ref('')
const { mutate: createOrganisation, isPending: isCreating } = useCreateOrganisation()
function slugify(text: string): string {
return text
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-+|-+$/g, '')
}
function openCreateDialog() {
createForm.value = { name: '', slug: '' }
createError.value = ''
isCreateDialogOpen.value = true
}
function onNameInput(name: string) {
createForm.value.name = name
createForm.value.slug = slugify(name)
}
function submitCreate() {
createError.value = ''
createOrganisation(createForm.value, {
onSuccess: (org) => {
isCreateDialogOpen.value = false
router.push({ name: 'platform-organisations-id', params: { id: org.id } })
},
onError: (err: unknown) => {
const error = err as { response?: { data?: { message?: string } } }
createError.value = error.response?.data?.message ?? 'Er is een fout opgetreden.'
},
})
}
</script>
<template>
<div>
<div class="d-flex align-center justify-space-between mb-6">
<div>
<h4 class="text-h4">
Organisaties
</h4>
<p class="text-body-1 text-disabled mb-0">
Alle organisaties op het platform
</p>
</div>
<VBtn
prepend-icon="tabler-plus"
@click="openCreateDialog"
>
Nieuwe organisatie
</VBtn>
</div>
<!-- Error -->
<VAlert
v-if="isError"
type="error"
class="mb-4"
>
Kon organisaties niet laden.
<template #append>
<VBtn
variant="text"
@click="refetch()"
>
Opnieuw proberen
</VBtn>
</template>
</VAlert>
<VCard>
<!-- Filters -->
<VCardText>
<VRow>
<VCol
cols="12"
md="6"
>
<AppTextField
v-model="search"
placeholder="Zoek op naam of slug..."
prepend-inner-icon="tabler-search"
clearable
/>
</VCol>
<VCol
cols="12"
md="3"
>
<AppSelect
v-model="billingStatusFilter"
:items="billingStatusOptions"
placeholder="Status"
clearable
/>
</VCol>
</VRow>
</VCardText>
<VDataTableServer
:headers="headers"
:items="organisations"
:items-length="totalItems"
:loading="isLoading"
:items-per-page="itemsPerPage"
:page="page"
hover
@update:options="onUpdateOptions"
@click:row="onRowClick"
>
<template #item.billing_status="{ item }">
<VChip
:color="billingStatusColor[item.billing_status]"
size="small"
>
{{ item.billing_status_label ?? item.billing_status }}
</VChip>
</template>
<template #item.created_at="{ item }">
{{ formatDate(item.created_at) }}
</template>
<!-- Empty -->
<template #no-data>
<div class="text-center pa-4 text-disabled">
<VIcon
icon="tabler-building-off"
size="48"
class="mb-2"
/>
<p>Geen organisaties gevonden</p>
</div>
</template>
</VDataTableServer>
</VCard>
<!-- Create Dialog -->
<VDialog
v-model="isCreateDialogOpen"
max-width="500"
>
<VCard title="Nieuwe organisatie">
<VCardText>
<VAlert
v-if="createError"
type="error"
variant="tonal"
class="mb-4"
density="comfortable"
>
{{ createError }}
</VAlert>
<VRow>
<VCol cols="12">
<AppTextField
:model-value="createForm.name"
label="Naam"
@update:model-value="onNameInput"
/>
</VCol>
<VCol cols="12">
<AppTextField
v-model="createForm.slug"
label="Slug"
hint="Wordt automatisch gegenereerd"
/>
</VCol>
</VRow>
</VCardText>
<VCardActions>
<VSpacer />
<VBtn
variant="tonal"
@click="isCreateDialogOpen = false"
>
Annuleren
</VBtn>
<VBtn
color="primary"
:loading="isCreating"
:disabled="!createForm.name || !createForm.slug"
@click="submitCreate"
>
Aanmaken
</VBtn>
</VCardActions>
</VCard>
</VDialog>
</div>
</template>