feat: crowd types management UI with create/edit/deactivate
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -20,7 +20,10 @@ final class CrowdTypeController extends Controller
|
|||||||
{
|
{
|
||||||
Gate::authorize('viewAny', [CrowdType::class, $organisation]);
|
Gate::authorize('viewAny', [CrowdType::class, $organisation]);
|
||||||
|
|
||||||
$crowdTypes = $organisation->crowdTypes()->where('is_active', true)->get();
|
$crowdTypes = $organisation->crowdTypes()
|
||||||
|
->orderByDesc('is_active')
|
||||||
|
->orderBy('name')
|
||||||
|
->get();
|
||||||
|
|
||||||
return CrowdTypeResource::collection($crowdTypes);
|
return CrowdTypeResource::collection($crowdTypes);
|
||||||
}
|
}
|
||||||
@@ -47,11 +50,7 @@ final class CrowdTypeController extends Controller
|
|||||||
{
|
{
|
||||||
Gate::authorize('delete', [$crowdType, $organisation]);
|
Gate::authorize('delete', [$crowdType, $organisation]);
|
||||||
|
|
||||||
if ($crowdType->persons()->exists()) {
|
$crowdType->update(['is_active' => false]);
|
||||||
$crowdType->update(['is_active' => false]);
|
|
||||||
} else {
|
|
||||||
$crowdType->delete();
|
|
||||||
}
|
|
||||||
|
|
||||||
return response()->json(null, 204);
|
return response()->json(null, 204);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ declare(strict_types=1);
|
|||||||
namespace App\Http\Requests\Api\V1;
|
namespace App\Http\Requests\Api\V1;
|
||||||
|
|
||||||
use Illuminate\Foundation\Http\FormRequest;
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
use Illuminate\Validation\Rule;
|
||||||
|
|
||||||
final class StoreCrowdTypeRequest extends FormRequest
|
final class StoreCrowdTypeRequest extends FormRequest
|
||||||
{
|
{
|
||||||
@@ -17,7 +18,12 @@ final class StoreCrowdTypeRequest extends FormRequest
|
|||||||
public function rules(): array
|
public function rules(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'name' => ['required', 'string', 'max:100'],
|
'name' => [
|
||||||
|
'required',
|
||||||
|
'string',
|
||||||
|
'max:100',
|
||||||
|
Rule::unique('crowd_types')->where('organisation_id', $this->route('organisation')->id),
|
||||||
|
],
|
||||||
'system_type' => ['required', 'in:CREW,GUEST,ARTIST,VOLUNTEER,PRESS,PARTNER,SUPPLIER'],
|
'system_type' => ['required', 'in:CREW,GUEST,ARTIST,VOLUNTEER,PRESS,PARTNER,SUPPLIER'],
|
||||||
'color' => ['required', 'regex:/^#[0-9A-Fa-f]{6}$/'],
|
'color' => ['required', 'regex:/^#[0-9A-Fa-f]{6}$/'],
|
||||||
'icon' => ['nullable', 'string', 'max:50'],
|
'icon' => ['nullable', 'string', 'max:50'],
|
||||||
|
|||||||
@@ -123,10 +123,11 @@ class CrowdTypeTest extends TestCase
|
|||||||
->assertJson(['data' => ['name' => 'New Name', 'color' => '#ff0000']]);
|
->assertJson(['data' => ['name' => 'New Name', 'color' => '#ff0000']]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_destroy_deletes_crowd_type_without_persons(): void
|
public function test_destroy_deactivates_crowd_type(): void
|
||||||
{
|
{
|
||||||
$crowdType = CrowdType::factory()->create([
|
$crowdType = CrowdType::factory()->create([
|
||||||
'organisation_id' => $this->organisation->id,
|
'organisation_id' => $this->organisation->id,
|
||||||
|
'is_active' => true,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
Sanctum::actingAs($this->orgAdmin);
|
Sanctum::actingAs($this->orgAdmin);
|
||||||
@@ -134,6 +135,47 @@ class CrowdTypeTest extends TestCase
|
|||||||
$response = $this->deleteJson("/api/v1/organisations/{$this->organisation->id}/crowd-types/{$crowdType->id}");
|
$response = $this->deleteJson("/api/v1/organisations/{$this->organisation->id}/crowd-types/{$crowdType->id}");
|
||||||
|
|
||||||
$response->assertNoContent();
|
$response->assertNoContent();
|
||||||
$this->assertDatabaseMissing('crowd_types', ['id' => $crowdType->id]);
|
$this->assertDatabaseHas('crowd_types', [
|
||||||
|
'id' => $crowdType->id,
|
||||||
|
'is_active' => false,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_store_duplicate_name_returns_422(): void
|
||||||
|
{
|
||||||
|
CrowdType::factory()->create([
|
||||||
|
'organisation_id' => $this->organisation->id,
|
||||||
|
'name' => 'Vrijwilliger',
|
||||||
|
]);
|
||||||
|
|
||||||
|
Sanctum::actingAs($this->orgAdmin);
|
||||||
|
|
||||||
|
$response = $this->postJson("/api/v1/organisations/{$this->organisation->id}/crowd-types", [
|
||||||
|
'name' => 'Vrijwilliger',
|
||||||
|
'system_type' => 'VOLUNTEER',
|
||||||
|
'color' => '#10b981',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response->assertUnprocessable()
|
||||||
|
->assertJsonValidationErrors('name');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_index_includes_inactive_crowd_types(): void
|
||||||
|
{
|
||||||
|
CrowdType::factory()->create([
|
||||||
|
'organisation_id' => $this->organisation->id,
|
||||||
|
'is_active' => true,
|
||||||
|
]);
|
||||||
|
CrowdType::factory()->create([
|
||||||
|
'organisation_id' => $this->organisation->id,
|
||||||
|
'is_active' => false,
|
||||||
|
]);
|
||||||
|
|
||||||
|
Sanctum::actingAs($this->orgAdmin);
|
||||||
|
|
||||||
|
$response = $this->getJson("/api/v1/organisations/{$this->organisation->id}/crowd-types");
|
||||||
|
|
||||||
|
$response->assertOk();
|
||||||
|
$this->assertCount(2, $response->json('data'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
393
apps/app/src/components/organisations/CrowdTypesManager.vue
Normal file
393
apps/app/src/components/organisations/CrowdTypesManager.vue
Normal file
@@ -0,0 +1,393 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { VForm } from 'vuetify/components/VForm'
|
||||||
|
import { useCrowdTypeList, useCreateCrowdType, useUpdateCrowdType, useDeleteCrowdType } from '@/composables/api/useCrowdTypes'
|
||||||
|
import { requiredValidator } from '@core/utils/validators'
|
||||||
|
import type { CrowdType } from '@/types/organisation'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
orgId: string
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const orgIdRef = computed(() => props.orgId)
|
||||||
|
|
||||||
|
const { data: crowdTypes, isLoading } = useCrowdTypeList(orgIdRef)
|
||||||
|
const { mutate: createCrowdType, isPending: isCreating } = useCreateCrowdType(orgIdRef)
|
||||||
|
const { mutate: updateCrowdType, isPending: isUpdating } = useUpdateCrowdType(orgIdRef)
|
||||||
|
const { mutate: deleteCrowdType } = useDeleteCrowdType(orgIdRef)
|
||||||
|
|
||||||
|
const isDialogOpen = ref(false)
|
||||||
|
const editingCrowdType = ref<CrowdType | null>(null)
|
||||||
|
const errors = ref<Record<string, string>>({})
|
||||||
|
const refVForm = ref<VForm>()
|
||||||
|
const showSuccess = ref(false)
|
||||||
|
const successMessage = ref('')
|
||||||
|
|
||||||
|
const systemTypeOptions = [
|
||||||
|
{ title: 'Crew', value: 'CREW' },
|
||||||
|
{ title: 'Gast', value: 'GUEST' },
|
||||||
|
{ title: 'Artiest', value: 'ARTIST' },
|
||||||
|
{ title: 'Vrijwilliger', value: 'VOLUNTEER' },
|
||||||
|
{ title: 'Pers', value: 'PRESS' },
|
||||||
|
{ title: 'Partner', value: 'PARTNER' },
|
||||||
|
{ title: 'Leverancier', value: 'SUPPLIER' },
|
||||||
|
]
|
||||||
|
|
||||||
|
const systemTypeLabels: Record<string, string> = {
|
||||||
|
CREW: 'Crew',
|
||||||
|
GUEST: 'Gast',
|
||||||
|
ARTIST: 'Artiest',
|
||||||
|
VOLUNTEER: 'Vrijwilliger',
|
||||||
|
PRESS: 'Pers',
|
||||||
|
PARTNER: 'Partner',
|
||||||
|
SUPPLIER: 'Leverancier',
|
||||||
|
}
|
||||||
|
|
||||||
|
const form = ref({
|
||||||
|
name: '',
|
||||||
|
system_type: '',
|
||||||
|
color: '#3b82f6',
|
||||||
|
icon: '',
|
||||||
|
})
|
||||||
|
|
||||||
|
const activeCrowdTypes = computed(() =>
|
||||||
|
crowdTypes.value?.filter(ct => ct.is_active) ?? [],
|
||||||
|
)
|
||||||
|
|
||||||
|
const inactiveCrowdTypes = computed(() =>
|
||||||
|
crowdTypes.value?.filter(ct => !ct.is_active) ?? [],
|
||||||
|
)
|
||||||
|
|
||||||
|
const dialogTitle = computed(() =>
|
||||||
|
editingCrowdType.value ? 'Crowd type bewerken' : 'Crowd type aanmaken',
|
||||||
|
)
|
||||||
|
|
||||||
|
const isSaving = computed(() => isCreating.value || isUpdating.value)
|
||||||
|
|
||||||
|
function openCreateDialog() {
|
||||||
|
editingCrowdType.value = null
|
||||||
|
form.value = { name: '', system_type: '', color: '#3b82f6', icon: '' }
|
||||||
|
errors.value = {}
|
||||||
|
isDialogOpen.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function openEditDialog(ct: CrowdType) {
|
||||||
|
editingCrowdType.value = ct
|
||||||
|
form.value = {
|
||||||
|
name: ct.name,
|
||||||
|
system_type: ct.system_type,
|
||||||
|
color: ct.color,
|
||||||
|
icon: ct.icon ?? '',
|
||||||
|
}
|
||||||
|
errors.value = {}
|
||||||
|
isDialogOpen.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function onSubmit() {
|
||||||
|
refVForm.value?.validate().then(({ valid }) => {
|
||||||
|
if (!valid) return
|
||||||
|
|
||||||
|
errors.value = {}
|
||||||
|
const payload = {
|
||||||
|
name: form.value.name,
|
||||||
|
system_type: form.value.system_type,
|
||||||
|
color: form.value.color,
|
||||||
|
...(form.value.icon ? { icon: form.value.icon } : { icon: null }),
|
||||||
|
}
|
||||||
|
|
||||||
|
if (editingCrowdType.value) {
|
||||||
|
updateCrowdType(
|
||||||
|
{ id: editingCrowdType.value.id, ...payload },
|
||||||
|
{
|
||||||
|
onSuccess: () => {
|
||||||
|
isDialogOpen.value = false
|
||||||
|
successMessage.value = `${form.value.name} bijgewerkt`
|
||||||
|
showSuccess.value = true
|
||||||
|
},
|
||||||
|
onError: handleError,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
createCrowdType(payload, {
|
||||||
|
onSuccess: () => {
|
||||||
|
isDialogOpen.value = false
|
||||||
|
successMessage.value = `${form.value.name} aangemaakt`
|
||||||
|
showSuccess.value = true
|
||||||
|
},
|
||||||
|
onError: handleError,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleError(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 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function deactivate(ct: CrowdType) {
|
||||||
|
deleteCrowdType(ct.id, {
|
||||||
|
onSuccess: () => {
|
||||||
|
successMessage.value = `${ct.name} gedeactiveerd`
|
||||||
|
showSuccess.value = true
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function activate(ct: CrowdType) {
|
||||||
|
updateCrowdType(
|
||||||
|
{ id: ct.id, is_active: true },
|
||||||
|
{
|
||||||
|
onSuccess: () => {
|
||||||
|
successMessage.value = `${ct.name} geactiveerd`
|
||||||
|
showSuccess.value = true
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<VCard>
|
||||||
|
<VCardItem>
|
||||||
|
<VCardTitle>Crowd types</VCardTitle>
|
||||||
|
<VCardSubtitle>Definieer de deelnemertypes voor je organisatie</VCardSubtitle>
|
||||||
|
<template #append>
|
||||||
|
<VBtn
|
||||||
|
prepend-icon="tabler-plus"
|
||||||
|
@click="openCreateDialog"
|
||||||
|
>
|
||||||
|
Crowd type toevoegen
|
||||||
|
</VBtn>
|
||||||
|
</template>
|
||||||
|
</VCardItem>
|
||||||
|
|
||||||
|
<VCardText>
|
||||||
|
<!-- Loading -->
|
||||||
|
<VSkeletonLoader
|
||||||
|
v-if="isLoading"
|
||||||
|
type="list-item@3"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<template v-else>
|
||||||
|
<!-- Empty state -->
|
||||||
|
<VAlert
|
||||||
|
v-if="!crowdTypes?.length"
|
||||||
|
type="info"
|
||||||
|
variant="tonal"
|
||||||
|
>
|
||||||
|
Nog geen crowd types aangemaakt. Maak er een aan om personen te categoriseren.
|
||||||
|
</VAlert>
|
||||||
|
|
||||||
|
<!-- Active crowd types -->
|
||||||
|
<VList
|
||||||
|
v-if="activeCrowdTypes.length"
|
||||||
|
lines="one"
|
||||||
|
>
|
||||||
|
<VListItem
|
||||||
|
v-for="ct in activeCrowdTypes"
|
||||||
|
:key="ct.id"
|
||||||
|
>
|
||||||
|
<template #prepend>
|
||||||
|
<VAvatar
|
||||||
|
:color="ct.color"
|
||||||
|
size="32"
|
||||||
|
variant="flat"
|
||||||
|
>
|
||||||
|
<VIcon
|
||||||
|
v-if="ct.icon"
|
||||||
|
:icon="ct.icon"
|
||||||
|
size="18"
|
||||||
|
color="white"
|
||||||
|
/>
|
||||||
|
</VAvatar>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<VListItemTitle>{{ ct.name }}</VListItemTitle>
|
||||||
|
<VListItemSubtitle>
|
||||||
|
<VChip
|
||||||
|
size="x-small"
|
||||||
|
variant="tonal"
|
||||||
|
class="mt-1"
|
||||||
|
>
|
||||||
|
{{ systemTypeLabels[ct.system_type] ?? ct.system_type }}
|
||||||
|
</VChip>
|
||||||
|
</VListItemSubtitle>
|
||||||
|
|
||||||
|
<template #append>
|
||||||
|
<VBtn
|
||||||
|
icon="tabler-edit"
|
||||||
|
variant="text"
|
||||||
|
size="small"
|
||||||
|
@click="openEditDialog(ct)"
|
||||||
|
/>
|
||||||
|
<VBtn
|
||||||
|
icon="tabler-eye-off"
|
||||||
|
variant="text"
|
||||||
|
size="small"
|
||||||
|
color="warning"
|
||||||
|
@click="deactivate(ct)"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</VListItem>
|
||||||
|
</VList>
|
||||||
|
|
||||||
|
<!-- Inactive crowd types -->
|
||||||
|
<template v-if="inactiveCrowdTypes.length">
|
||||||
|
<VDivider class="my-4" />
|
||||||
|
<p class="text-body-2 text-disabled mb-2">
|
||||||
|
Inactief
|
||||||
|
</p>
|
||||||
|
<VList
|
||||||
|
lines="one"
|
||||||
|
class="opacity-60"
|
||||||
|
>
|
||||||
|
<VListItem
|
||||||
|
v-for="ct in inactiveCrowdTypes"
|
||||||
|
:key="ct.id"
|
||||||
|
>
|
||||||
|
<template #prepend>
|
||||||
|
<VAvatar
|
||||||
|
:color="ct.color"
|
||||||
|
size="32"
|
||||||
|
variant="flat"
|
||||||
|
>
|
||||||
|
<VIcon
|
||||||
|
v-if="ct.icon"
|
||||||
|
:icon="ct.icon"
|
||||||
|
size="18"
|
||||||
|
color="white"
|
||||||
|
/>
|
||||||
|
</VAvatar>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<VListItemTitle class="text-disabled">
|
||||||
|
{{ ct.name }}
|
||||||
|
</VListItemTitle>
|
||||||
|
<VListItemSubtitle>
|
||||||
|
<VChip
|
||||||
|
size="x-small"
|
||||||
|
variant="tonal"
|
||||||
|
class="mt-1"
|
||||||
|
>
|
||||||
|
{{ systemTypeLabels[ct.system_type] ?? ct.system_type }}
|
||||||
|
</VChip>
|
||||||
|
</VListItemSubtitle>
|
||||||
|
|
||||||
|
<template #append>
|
||||||
|
<VBtn
|
||||||
|
variant="tonal"
|
||||||
|
size="small"
|
||||||
|
color="success"
|
||||||
|
@click="activate(ct)"
|
||||||
|
>
|
||||||
|
Activeren
|
||||||
|
</VBtn>
|
||||||
|
</template>
|
||||||
|
</VListItem>
|
||||||
|
</VList>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
</VCardText>
|
||||||
|
</VCard>
|
||||||
|
|
||||||
|
<!-- Create / Edit dialog -->
|
||||||
|
<VDialog
|
||||||
|
v-model="isDialogOpen"
|
||||||
|
max-width="500"
|
||||||
|
>
|
||||||
|
<VCard :title="dialogTitle">
|
||||||
|
<VForm
|
||||||
|
ref="refVForm"
|
||||||
|
@submit.prevent="onSubmit"
|
||||||
|
>
|
||||||
|
<VCardText>
|
||||||
|
<VRow>
|
||||||
|
<VCol cols="12">
|
||||||
|
<AppTextField
|
||||||
|
v-model="form.name"
|
||||||
|
label="Naam"
|
||||||
|
:rules="[requiredValidator]"
|
||||||
|
:error-messages="errors.name"
|
||||||
|
autofocus
|
||||||
|
autocomplete="one-time-code"
|
||||||
|
/>
|
||||||
|
</VCol>
|
||||||
|
<VCol cols="12">
|
||||||
|
<AppSelect
|
||||||
|
v-model="form.system_type"
|
||||||
|
label="Systeemtype"
|
||||||
|
:items="systemTypeOptions"
|
||||||
|
:rules="[requiredValidator]"
|
||||||
|
:error-messages="errors.system_type"
|
||||||
|
:disabled="!!editingCrowdType"
|
||||||
|
hint="Bepaalt hoe dit type wordt gebruikt in het systeem"
|
||||||
|
persistent-hint
|
||||||
|
/>
|
||||||
|
</VCol>
|
||||||
|
<VCol
|
||||||
|
cols="12"
|
||||||
|
md="6"
|
||||||
|
>
|
||||||
|
<label class="text-body-2 mb-1 d-block">Kleur</label>
|
||||||
|
<input
|
||||||
|
v-model="form.color"
|
||||||
|
type="color"
|
||||||
|
class="w-100 rounded cursor-pointer"
|
||||||
|
style="block-size: 40px; border: 1px solid rgba(var(--v-border-color), var(--v-border-opacity));"
|
||||||
|
>
|
||||||
|
<p
|
||||||
|
v-if="errors.color"
|
||||||
|
class="text-error text-caption mt-1"
|
||||||
|
>
|
||||||
|
{{ errors.color }}
|
||||||
|
</p>
|
||||||
|
</VCol>
|
||||||
|
<VCol
|
||||||
|
cols="12"
|
||||||
|
md="6"
|
||||||
|
>
|
||||||
|
<AppTextField
|
||||||
|
v-model="form.icon"
|
||||||
|
label="Icoon"
|
||||||
|
placeholder="tabler-users"
|
||||||
|
:error-messages="errors.icon"
|
||||||
|
/>
|
||||||
|
</VCol>
|
||||||
|
</VRow>
|
||||||
|
</VCardText>
|
||||||
|
<VCardActions>
|
||||||
|
<VSpacer />
|
||||||
|
<VBtn
|
||||||
|
variant="text"
|
||||||
|
@click="isDialogOpen = false"
|
||||||
|
>
|
||||||
|
Annuleren
|
||||||
|
</VBtn>
|
||||||
|
<VBtn
|
||||||
|
type="submit"
|
||||||
|
color="primary"
|
||||||
|
:loading="isSaving"
|
||||||
|
>
|
||||||
|
Opslaan
|
||||||
|
</VBtn>
|
||||||
|
</VCardActions>
|
||||||
|
</VForm>
|
||||||
|
</VCard>
|
||||||
|
</VDialog>
|
||||||
|
|
||||||
|
<VSnackbar
|
||||||
|
v-model="showSuccess"
|
||||||
|
color="success"
|
||||||
|
:timeout="3000"
|
||||||
|
>
|
||||||
|
{{ successMessage }}
|
||||||
|
</VSnackbar>
|
||||||
|
</template>
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useQuery } from '@tanstack/vue-query'
|
import { useMutation, useQuery, useQueryClient } from '@tanstack/vue-query'
|
||||||
import type { Ref } from 'vue'
|
import type { Ref } from 'vue'
|
||||||
import { apiClient } from '@/lib/axios'
|
import { apiClient } from '@/lib/axios'
|
||||||
import type { CrowdType } from '@/types/organisation'
|
import type { CrowdType } from '@/types/organisation'
|
||||||
@@ -14,6 +14,12 @@ interface PaginatedResponse<T> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface ApiResponse<T> {
|
||||||
|
success: boolean
|
||||||
|
data: T
|
||||||
|
message?: string
|
||||||
|
}
|
||||||
|
|
||||||
export function useCrowdTypeList(orgId: Ref<string>) {
|
export function useCrowdTypeList(orgId: Ref<string>) {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: ['crowd-types', orgId],
|
queryKey: ['crowd-types', orgId],
|
||||||
@@ -28,3 +34,50 @@ export function useCrowdTypeList(orgId: Ref<string>) {
|
|||||||
staleTime: Infinity,
|
staleTime: Infinity,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function useCreateCrowdType(orgId: Ref<string>) {
|
||||||
|
const queryClient = useQueryClient()
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: async (payload: { name: string; system_type: string; color: string; icon?: string | null }) => {
|
||||||
|
const { data } = await apiClient.post<ApiResponse<CrowdType>>(
|
||||||
|
`/organisations/${orgId.value}/crowd-types`,
|
||||||
|
payload,
|
||||||
|
)
|
||||||
|
return data.data
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['crowd-types', orgId] })
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useUpdateCrowdType(orgId: Ref<string>) {
|
||||||
|
const queryClient = useQueryClient()
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: async ({ id, ...payload }: { id: string; name?: string; color?: string; icon?: string | null; is_active?: boolean }) => {
|
||||||
|
const { data } = await apiClient.put<ApiResponse<CrowdType>>(
|
||||||
|
`/organisations/${orgId.value}/crowd-types/${id}`,
|
||||||
|
payload,
|
||||||
|
)
|
||||||
|
return data.data
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['crowd-types', orgId] })
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useDeleteCrowdType(orgId: Ref<string>) {
|
||||||
|
const queryClient = useQueryClient()
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: async (id: string) => {
|
||||||
|
await apiClient.delete(`/organisations/${orgId.value}/crowd-types/${id}`)
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['crowd-types', orgId] })
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
import { useMyOrganisation } from '@/composables/api/useOrganisations'
|
import { useMyOrganisation } from '@/composables/api/useOrganisations'
|
||||||
import { useAuthStore } from '@/stores/useAuthStore'
|
import { useAuthStore } from '@/stores/useAuthStore'
|
||||||
import EditOrganisationDialog from '@/components/organisations/EditOrganisationDialog.vue'
|
import EditOrganisationDialog from '@/components/organisations/EditOrganisationDialog.vue'
|
||||||
|
import CrowdTypesManager from '@/components/organisations/CrowdTypesManager.vue'
|
||||||
import type { Organisation } from '@/types/organisation'
|
import type { Organisation } from '@/types/organisation'
|
||||||
|
|
||||||
const authStore = useAuthStore()
|
const authStore = useAuthStore()
|
||||||
@@ -123,6 +124,13 @@ function formatDate(iso: string) {
|
|||||||
</VCardText>
|
</VCardText>
|
||||||
</VCard>
|
</VCard>
|
||||||
|
|
||||||
|
<!-- Crowd Types -->
|
||||||
|
<CrowdTypesManager
|
||||||
|
v-if="isOrgAdmin"
|
||||||
|
:org-id="organisation.id"
|
||||||
|
class="mt-6"
|
||||||
|
/>
|
||||||
|
|
||||||
<EditOrganisationDialog
|
<EditOrganisationDialog
|
||||||
v-model="isEditDialogOpen"
|
v-model="isEditDialogOpen"
|
||||||
:organisation="organisation"
|
:organisation="organisation"
|
||||||
|
|||||||
Reference in New Issue
Block a user