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>
276 lines
7.4 KiB
Vue
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>
|