diff --git a/api/app/Http/Controllers/Api/V1/CrowdTypeController.php b/api/app/Http/Controllers/Api/V1/CrowdTypeController.php
index 3045299..df38cfc 100644
--- a/api/app/Http/Controllers/Api/V1/CrowdTypeController.php
+++ b/api/app/Http/Controllers/Api/V1/CrowdTypeController.php
@@ -20,7 +20,10 @@ final class CrowdTypeController extends Controller
{
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);
}
@@ -47,11 +50,7 @@ final class CrowdTypeController extends Controller
{
Gate::authorize('delete', [$crowdType, $organisation]);
- if ($crowdType->persons()->exists()) {
- $crowdType->update(['is_active' => false]);
- } else {
- $crowdType->delete();
- }
+ $crowdType->update(['is_active' => false]);
return response()->json(null, 204);
}
diff --git a/api/app/Http/Requests/Api/V1/StoreCrowdTypeRequest.php b/api/app/Http/Requests/Api/V1/StoreCrowdTypeRequest.php
index 4ddfce6..e5c1ef0 100644
--- a/api/app/Http/Requests/Api/V1/StoreCrowdTypeRequest.php
+++ b/api/app/Http/Requests/Api/V1/StoreCrowdTypeRequest.php
@@ -5,6 +5,7 @@ declare(strict_types=1);
namespace App\Http\Requests\Api\V1;
use Illuminate\Foundation\Http\FormRequest;
+use Illuminate\Validation\Rule;
final class StoreCrowdTypeRequest extends FormRequest
{
@@ -17,7 +18,12 @@ final class StoreCrowdTypeRequest extends FormRequest
public function rules(): array
{
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'],
'color' => ['required', 'regex:/^#[0-9A-Fa-f]{6}$/'],
'icon' => ['nullable', 'string', 'max:50'],
diff --git a/api/tests/Feature/CrowdType/CrowdTypeTest.php b/api/tests/Feature/CrowdType/CrowdTypeTest.php
index 5165e5e..f855b48 100644
--- a/api/tests/Feature/CrowdType/CrowdTypeTest.php
+++ b/api/tests/Feature/CrowdType/CrowdTypeTest.php
@@ -123,10 +123,11 @@ class CrowdTypeTest extends TestCase
->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([
'organisation_id' => $this->organisation->id,
+ 'is_active' => true,
]);
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->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'));
}
}
diff --git a/apps/app/src/components/organisations/CrowdTypesManager.vue b/apps/app/src/components/organisations/CrowdTypesManager.vue
new file mode 100644
index 0000000..44c762b
--- /dev/null
+++ b/apps/app/src/components/organisations/CrowdTypesManager.vue
@@ -0,0 +1,393 @@
+
+
+
+
+
+ Crowd types
+ Definieer de deelnemertypes voor je organisatie
+
+
+ Crowd type toevoegen
+
+
+
+
+
+
+
+
+
+
+
+ Nog geen crowd types aangemaakt. Maak er een aan om personen te categoriseren.
+
+
+
+
+
+
+
+
+
+
+
+ {{ ct.name }}
+
+
+ {{ systemTypeLabels[ct.system_type] ?? ct.system_type }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Inactief
+
+
+
+
+
+
+
+
+
+
+ {{ ct.name }}
+
+
+
+ {{ systemTypeLabels[ct.system_type] ?? ct.system_type }}
+
+
+
+
+
+ Activeren
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ errors.color }}
+
+
+
+
+
+
+
+
+
+
+ Annuleren
+
+
+ Opslaan
+
+
+
+
+
+
+
+ {{ successMessage }}
+
+
diff --git a/apps/app/src/composables/api/useCrowdTypes.ts b/apps/app/src/composables/api/useCrowdTypes.ts
index 10a4e8d..ea79665 100644
--- a/apps/app/src/composables/api/useCrowdTypes.ts
+++ b/apps/app/src/composables/api/useCrowdTypes.ts
@@ -1,4 +1,4 @@
-import { useQuery } from '@tanstack/vue-query'
+import { useMutation, useQuery, useQueryClient } from '@tanstack/vue-query'
import type { Ref } from 'vue'
import { apiClient } from '@/lib/axios'
import type { CrowdType } from '@/types/organisation'
@@ -14,6 +14,12 @@ interface PaginatedResponse {
}
}
+interface ApiResponse {
+ success: boolean
+ data: T
+ message?: string
+}
+
export function useCrowdTypeList(orgId: Ref) {
return useQuery({
queryKey: ['crowd-types', orgId],
@@ -28,3 +34,50 @@ export function useCrowdTypeList(orgId: Ref) {
staleTime: Infinity,
})
}
+
+export function useCreateCrowdType(orgId: Ref) {
+ const queryClient = useQueryClient()
+
+ return useMutation({
+ mutationFn: async (payload: { name: string; system_type: string; color: string; icon?: string | null }) => {
+ const { data } = await apiClient.post>(
+ `/organisations/${orgId.value}/crowd-types`,
+ payload,
+ )
+ return data.data
+ },
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: ['crowd-types', orgId] })
+ },
+ })
+}
+
+export function useUpdateCrowdType(orgId: Ref) {
+ 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>(
+ `/organisations/${orgId.value}/crowd-types/${id}`,
+ payload,
+ )
+ return data.data
+ },
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: ['crowd-types', orgId] })
+ },
+ })
+}
+
+export function useDeleteCrowdType(orgId: Ref) {
+ 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] })
+ },
+ })
+}
diff --git a/apps/app/src/pages/organisation/index.vue b/apps/app/src/pages/organisation/index.vue
index 844c726..0beb081 100644
--- a/apps/app/src/pages/organisation/index.vue
+++ b/apps/app/src/pages/organisation/index.vue
@@ -2,6 +2,7 @@
import { useMyOrganisation } from '@/composables/api/useOrganisations'
import { useAuthStore } from '@/stores/useAuthStore'
import EditOrganisationDialog from '@/components/organisations/EditOrganisationDialog.vue'
+import CrowdTypesManager from '@/components/organisations/CrowdTypesManager.vue'
import type { Organisation } from '@/types/organisation'
const authStore = useAuthStore()
@@ -123,6 +124,13 @@ function formatDate(iso: string) {
+
+
+