feat: companies CRUD with person dialog integration and navigation

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-10 11:16:01 +02:00
parent 169a078a92
commit 4388811be9
14 changed files with 984 additions and 73 deletions

View File

@@ -0,0 +1,301 @@
<script setup lang="ts">
import { useCompanies, useDeleteCompany } from '@/composables/api/useCompanies'
import { useAuthStore } from '@/stores/useAuthStore'
import { useOrganisationStore } from '@/stores/useOrganisationStore'
import CompanyDialog from '@/components/organisation/CompanyDialog.vue'
import type { Company } from '@/types/organisation'
const authStore = useAuthStore()
const orgStore = useOrganisationStore()
const orgId = computed(() => orgStore.activeOrganisationId ?? '')
const { data: companies, isLoading, isError, refetch } = useCompanies(orgId)
const { mutate: deleteCompany, isPending: isDeleting } = useDeleteCompany(orgId)
// Search & filter
const search = ref('')
const filterType = ref('')
const typeOptions = [
{ title: 'Alle types', value: '' },
{ title: 'Leverancier', value: 'supplier' },
{ title: 'Partner', value: 'partner' },
{ title: 'Bureau', value: 'agency' },
{ title: 'Locatie', value: 'venue' },
{ title: 'Overig', value: 'other' },
]
const typeLabel: Record<string, string> = {
supplier: 'Leverancier',
partner: 'Partner',
agency: 'Bureau',
venue: 'Locatie',
other: 'Overig',
}
const typeColor: Record<string, string> = {
supplier: 'info',
partner: 'success',
agency: 'purple',
venue: 'warning',
other: 'default',
}
const filteredCompanies = computed(() => {
let result = companies.value ?? []
if (filterType.value) {
result = result.filter(c => c.type === filterType.value)
}
if (search.value) {
const q = search.value.toLowerCase()
result = result.filter(c => c.name.toLowerCase().includes(q))
}
return result
})
const headers = [
{ title: 'Naam', key: 'name' },
{ title: 'Type', key: 'type' },
{ title: 'Contactpersoon', key: 'contact_name' },
{ title: 'E-mail', key: 'contact_email' },
{ title: 'Telefoon', key: 'contact_phone' },
{ title: 'Acties', key: 'actions', sortable: false, align: 'end' as const },
]
// Dialogs
const isDialogOpen = ref(false)
const editingCompany = ref<Company | null>(null)
const isDeleteDialogOpen = ref(false)
const deletingCompany = ref<Company | null>(null)
const showSuccess = ref(false)
const successMessage = ref('')
function onAdd() {
editingCompany.value = null
isDialogOpen.value = true
}
function onEdit(company: Company) {
editingCompany.value = company
isDialogOpen.value = true
}
function onDeleteConfirm(company: Company) {
deletingCompany.value = company
isDeleteDialogOpen.value = true
}
function onDeleteExecute() {
if (!deletingCompany.value) return
const name = deletingCompany.value.name
deleteCompany(deletingCompany.value.id, {
onSuccess: () => {
isDeleteDialogOpen.value = false
deletingCompany.value = null
successMessage.value = `${name} verwijderd`
showSuccess.value = true
},
})
}
function onSaved() {
successMessage.value = editingCompany.value ? 'Bedrijf bijgewerkt' : 'Bedrijf toegevoegd'
showSuccess.value = true
}
</script>
<template>
<div>
<!-- Loading -->
<VSkeletonLoader
v-if="isLoading"
type="card"
/>
<!-- Error -->
<VAlert
v-else-if="isError"
type="error"
class="mb-4"
>
Kon bedrijven niet laden.
<template #append>
<VBtn
variant="text"
@click="refetch()"
>
Opnieuw proberen
</VBtn>
</template>
</VAlert>
<template v-else>
<!-- Header -->
<div class="d-flex justify-space-between align-center mb-6">
<div>
<h4 class="text-h4">
Bedrijven
</h4>
<p class="text-body-1 text-disabled mb-0">
Leveranciers, partners en andere organisaties
</p>
</div>
<VBtn
prepend-icon="tabler-plus"
@click="onAdd"
>
Bedrijf toevoegen
</VBtn>
</div>
<!-- Filters -->
<div class="d-flex gap-x-4 mb-4">
<AppTextField
v-model="search"
prepend-inner-icon="tabler-search"
placeholder="Zoek op naam..."
clearable
style="max-inline-size: 300px;"
/>
<AppSelect
v-model="filterType"
label="Type"
:items="typeOptions"
clearable
style="min-inline-size: 180px;"
/>
</div>
<!-- Empty state -->
<VCard
v-if="!companies?.length"
class="text-center pa-8"
>
<VIcon
icon="tabler-building"
size="48"
class="mb-4 text-disabled"
/>
<p class="text-body-1 text-disabled">
Nog geen bedrijven. Voeg je eerste bedrijf toe.
</p>
</VCard>
<!-- No results after filter -->
<VCard
v-else-if="!filteredCompanies.length"
class="text-center pa-8"
>
<p class="text-body-1 text-disabled">
Geen bedrijven gevonden voor deze zoekopdracht.
</p>
</VCard>
<!-- Data table -->
<VCard v-else>
<VDataTable
:headers="headers"
:items="filteredCompanies"
item-value="id"
:items-per-page="-1"
hide-default-footer
hover
@click:row="(_e: Event, row: { item: Company }) => onEdit(row.item)"
>
<template #item.type="{ item }">
<VChip
:color="typeColor[item.type] ?? 'default'"
size="small"
>
{{ typeLabel[item.type] ?? item.type }}
</VChip>
</template>
<template #item.contact_name="{ item }">
{{ item.contact_name ?? '-' }}
</template>
<template #item.contact_email="{ item }">
{{ item.contact_email ?? '-' }}
</template>
<template #item.contact_phone="{ item }">
{{ item.contact_phone ?? '-' }}
</template>
<template #item.actions="{ item }">
<div class="d-flex justify-end gap-x-1">
<VBtn
icon="tabler-edit"
variant="text"
size="small"
title="Bewerken"
@click.stop="onEdit(item)"
/>
<VBtn
icon="tabler-trash"
variant="text"
size="small"
color="error"
title="Verwijderen"
@click.stop="onDeleteConfirm(item)"
/>
</div>
</template>
</VDataTable>
</VCard>
</template>
<!-- Create/Edit dialog -->
<CompanyDialog
v-model="isDialogOpen"
:org-id="orgId"
:company="editingCompany"
@saved="onSaved"
/>
<!-- Delete confirmation -->
<VDialog
v-model="isDeleteDialogOpen"
max-width="400"
>
<VCard title="Bedrijf verwijderen">
<VCardText>
Weet je zeker dat je <strong>{{ deletingCompany?.name }}</strong> wilt verwijderen?
</VCardText>
<VCardActions>
<VSpacer />
<VBtn
variant="text"
@click="isDeleteDialogOpen = false"
>
Annuleren
</VBtn>
<VBtn
color="error"
:loading="isDeleting"
@click="onDeleteExecute"
>
Verwijderen
</VBtn>
</VCardActions>
</VCard>
</VDialog>
<!-- Success snackbar -->
<VSnackbar
v-model="showSuccess"
color="success"
:timeout="3000"
>
{{ successMessage }}
</VSnackbar>
</div>
</template>