Skip to content

Commit 7b4e276

Browse files
committed
feat: custom field positions
1 parent b30d288 commit 7b4e276

File tree

9 files changed

+132
-19
lines changed

9 files changed

+132
-19
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
-- CreateEnum
2+
CREATE TYPE "CustomFieldPosition" AS ENUM ('PUBLIC_PERSON', 'PUBLIC_ANMELDUNG', 'INTERN_PERSON', 'INTERN_ANMELDUNG', 'INTERN_VERANSTALTUNG', 'INTERN_AUSSCHREIBUNG');
3+
4+
-- AlterTable
5+
ALTER TABLE "CustomField" ADD COLUMN "positions" "CustomFieldPosition"[];

api/prisma/schema.prisma

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -318,6 +318,16 @@ model Activity {
318318
metadata Json @default("{}")
319319
}
320320

321+
enum CustomFieldPosition {
322+
PUBLIC_PERSON
323+
PUBLIC_ANMELDUNG
324+
325+
INTERN_PERSON
326+
INTERN_ANMELDUNG
327+
INTERN_VERANSTALTUNG
328+
INTERN_AUSSCHREIBUNG
329+
}
330+
321331
model CustomField {
322332
id Int @id @default(autoincrement())
323333
name String
@@ -327,6 +337,7 @@ model CustomField {
327337
options String[]
328338
role Role[]
329339
values CustomFieldValue[]
340+
positions CustomFieldPosition[]
330341
331342
veranstaltungId Int?
332343
unterveranstaltungId Int?

api/src/services/customFields/customFieldsVeranstaltungCreate.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { CustomFieldPosition } from '@prisma/client'
12
import { z } from 'zod'
23

34
import { CustomFieldNames } from '../../enumMappings'
@@ -16,13 +17,14 @@ export const customFieldsVeranstaltungCreate = defineProcedure({
1617
type: z.enum(CustomFieldNames),
1718
required: z.boolean(),
1819
options: z.array(z.string()),
20+
positions: z.nativeEnum(CustomFieldPosition).array(),
1921
}),
2022
}),
2123
async handler({ input }) {
2224
return await prisma.customField.create({
2325
data: {
24-
veranstaltungId: input.veranstaltungId,
2526
...input.data,
27+
veranstaltungId: input.veranstaltungId,
2628
},
2729
})
2830
},

api/src/services/customFields/customFieldsVeranstaltungUpdate.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { CustomFieldPosition } from '@prisma/client'
12
import { z } from 'zod'
23

34
import { CustomFieldNames } from '../../enumMappings'
@@ -16,6 +17,7 @@ export const customFieldsVeranstaltungUpdate = defineProcedure({
1617
type: z.enum(CustomFieldNames),
1718
required: z.boolean(),
1819
options: z.array(z.string()),
20+
positions: z.nativeEnum(CustomFieldPosition).array(),
1921
}),
2022
}),
2123
async handler({ input }) {

frontend/src/components/BasicInputs/BasicSelect.vue

Lines changed: 43 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<script setup lang="ts">
22
import { Listbox, ListboxButton, ListboxOptions, ListboxOption } from '@headlessui/vue'
3-
import { ChevronDownIcon } from '@heroicons/vue/24/outline'
3+
import { CheckIcon, ChevronDownIcon } from '@heroicons/vue/24/outline'
44
55
import useValidationModel from '../../composables/useValidationModel'
66
@@ -9,20 +9,40 @@ import { type BasicInputDefaultProps } from './defaultProps'
99
1010
export interface Option {
1111
label: string
12+
description?: string
1213
value: string | number
1314
disabled?: boolean
1415
}
1516
1617
const props = defineProps<
17-
BasicInputDefaultProps<string | number> & {
18+
BasicInputDefaultProps<string | number | string[] | number[]> & {
1819
options: Option[]
20+
// eslint-disable-next-line vue/no-unused-properties
21+
multiple?: boolean
1922
}
2023
>()
2124
2225
const emit = defineEmits<{
2326
(event: 'update:modelValue', eventArgs: string | number | undefined): void
2427
}>()
2528
const { model, errorMessage } = useValidationModel(props, emit)
29+
30+
function formatValue(modelValue: unknown) {
31+
if (!modelValue) {
32+
return props.placeholder || 'Bitte wählen...'
33+
}
34+
if (Array.isArray(modelValue)) {
35+
if (modelValue.length === 0) {
36+
return props.placeholder || 'Bitte wählen...'
37+
}
38+
return props.options
39+
.filter((o) => modelValue.includes(o.value))
40+
.map((o) => o.label)
41+
.join(', ')
42+
}
43+
44+
return modelValue
45+
}
2646
</script>
2747

2848
<template>
@@ -31,20 +51,25 @@ const { model, errorMessage } = useValidationModel(props, emit)
3151
v-if="label"
3252
class="font-medium"
3353
:for="id || name || label"
34-
>{{ label }}
54+
>
55+
<span>{{ label }}</span>
3556
<span
3657
v-if="required"
3758
class="text-danger-600"
38-
>*</span
39-
></label
40-
>
59+
>
60+
*
61+
</span>
62+
</label>
4163
<Listbox
4264
v-model="model"
4365
as="div"
4466
:name="id || name || label"
67+
:multiple="props.multiple"
4568
>
4669
<ListboxButton class="input-style flex items-center justify-between">
47-
{{ options.find((option) => option.value === modelValue)?.label || placeholder || 'Bitte wählen...' }}
70+
<span class="text-start">
71+
{{ formatValue(modelValue) }}
72+
</span>
4873
<ChevronDownIcon class="h-5 text-gray-500" />
4974
</ListboxButton>
5075
<div class="relative mt-1">
@@ -58,18 +83,27 @@ const { model, errorMessage } = useValidationModel(props, emit)
5883
>
5984
<ListboxOption
6085
v-for="option in options"
61-
v-slot="{ active }"
86+
v-slot="{ active, selected }"
6287
:key="option.value"
6388
:value="option.value"
6489
:disabled="option.disabled"
6590
>
6691
<div
6792
:class="[
6893
active ? 'bg-primary-600 text-white' : 'text-gray-900',
94+
selected ? 'bg-primary-200' : '',
6995
'relative cursor-default select-none px-3 py-2',
96+
'flex flex-row gap-x-4 items-center',
7097
]"
7198
>
72-
{{ option.label }}
99+
<CheckIcon
100+
class="h-4"
101+
:class="selected ? 'text-primary-600' : 'invisible'"
102+
/>
103+
<div class="flex flex-col">
104+
<span>{{ option.label }}</span>
105+
<span class="text-xs mb-1">{{ option.description }}</span>
106+
</div>
73107
</div>
74108
</ListboxOption>
75109
</ListboxOptions>

frontend/src/components/CustomFieldsTable.vue

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,17 @@ import { CheckIcon, CubeTransparentIcon, XMarkIcon } from '@heroicons/vue/24/out
33
import { useAsyncState } from '@vueuse/core'
44
import { useRouter } from 'vue-router'
55
6+
import Loading from './UIComponents/Loading.vue'
7+
68
import { apiClient } from '@/api'
9+
import { CustomFieldPositionMapping, CustomFieldsMapping, getEnumOptions } from '@codeanker/api'
710
811
const props = defineProps<{
912
veranstaltungId: number
1013
// columns?: string[]
1114
}>()
1215
13-
const { state: fields } = useAsyncState(async () => {
16+
const { state: fields, isLoading } = useAsyncState(async () => {
1417
return apiClient.customFields.list.query({
1518
// filter: {
1619
// veranstaltungId: props.veranstaltungId,
@@ -22,6 +25,19 @@ const { state: fields } = useAsyncState(async () => {
2225
}, [])
2326
2427
const router = useRouter()
28+
29+
type Field = (typeof fields.value)[number]
30+
31+
function formatType(field: Field) {
32+
return CustomFieldsMapping.find((m) => m.value === field.type)?.label
33+
}
34+
35+
function formatPositions(field: Field) {
36+
return getEnumOptions(CustomFieldPositionMapping)
37+
.filter((o) => field.positions.includes(o.value))
38+
.map((o) => o.label)
39+
.join(', ')
40+
}
2541
</script>
2642

2743
<template>
@@ -50,6 +66,12 @@ const router = useRouter()
5066
>
5167
Erforderlich?
5268
</th>
69+
<th
70+
scope="col"
71+
class="px-3 py-3.5 text-left text-sm font-semibold"
72+
>
73+
Positionen
74+
</th>
5375
</tr>
5476
</thead>
5577
<tbody class="divide-y divide-gray-200 dark:divide-gray-700 bg-white dark:bg-dark-primary">
@@ -70,7 +92,7 @@ const router = useRouter()
7092
<p class="text-xs text-gray-500">{{ field.description }}</p>
7193
</td>
7294
<td class="whitespace-nowrap py-5 pl-4 pr-3 text-sm">
73-
<div>{{ field.type }}</div>
95+
<div>{{ formatType(field) }}</div>
7496
</td>
7597
<td class="whitespace-nowrap py-5 pl-4 pr-3 text-sm">
7698
<CheckIcon
@@ -82,11 +104,17 @@ const router = useRouter()
82104
class="text-red-600 size-5"
83105
/>
84106
</td>
107+
<td class="whitespace-nowrap py-5 pl-4 pr-3 text-sm">
108+
<div>{{ formatPositions(field) }}</div>
109+
</td>
85110
</tr>
86111
</tbody>
87112
</table>
113+
<div v-if="isLoading">
114+
<Loading />
115+
</div>
88116
<div
89-
v-if="fields.length <= 0"
117+
v-else-if="fields.length <= 0"
90118
class="rounded-md bg-blue-50 dark:bg-blue-950 p-4 text-blue-500"
91119
>
92120
<div class="flex">

frontend/src/components/forms/customFields/CustomFieldsForm.vue

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,26 @@
22
import { ChevronDownIcon, ChevronUpIcon, TrashIcon } from '@heroicons/vue/24/outline'
33
import { computed, ref } from 'vue'
44
5-
import BasicCheckbox from '@/components/BasicInputs/BasicCheckbox.vue'
65
import BasicInput from '@/components/BasicInputs/BasicInput.vue'
76
import BasicSelect, { type Option } from '@/components/BasicInputs/BasicSelect.vue'
7+
import BasicSwitch from '@/components/BasicInputs/BasicSwitch.vue'
88
import Button from '@/components/UIComponents/Button.vue'
9-
import { CustomFields, CustomFieldsMapping, type CustomFieldType } from '@codeanker/api'
9+
import {
10+
CustomFieldPositionMapping,
11+
CustomFields,
12+
CustomFieldsMapping,
13+
getEnumOptions,
14+
CustomFieldPosition,
15+
type CustomFieldType,
16+
} from '@codeanker/api'
1017
1118
export interface ICustomFieldData {
1219
name: string
1320
description?: string
1421
type: CustomFieldType
1522
required: boolean
1623
options: string[]
24+
positions: CustomFieldPosition[]
1725
}
1826
1927
const props = withDefaults(
@@ -37,6 +45,8 @@ const model = computed({
3745
})
3846
3947
const typeOptions = ref<Option[]>(CustomFieldsMapping)
48+
const positionOptions = ref(getEnumOptions(CustomFieldPositionMapping))
49+
4050
const field = computed(() => CustomFields.find((f) => f.name === model.value.type))
4151
4252
function moveOptionUp(index: number) {
@@ -70,12 +80,27 @@ function moveOptionDown(index: number) {
7080
label="Beschreibung"
7181
placeholder="Eine Beschreibung oder ein Hilfstext"
7282
/>
73-
<div class="col-span-full">
74-
<BasicCheckbox
83+
<div>
84+
<label
85+
for="required"
86+
class="font-medium"
87+
>
88+
Erforderlich?
89+
</label>
90+
<BasicSwitch
7591
v-model="model.required"
76-
label="Erforderlich?"
92+
name="required"
93+
label="Soll dieses Feld verpflichtend sein?"
94+
class="mt-2"
7795
/>
7896
</div>
97+
<BasicSelect
98+
v-model="model.positions"
99+
label="Positionen"
100+
:options="positionOptions"
101+
multiple
102+
required
103+
/>
79104

80105
<div
81106
v-if="field?.hasOptions"
@@ -123,6 +148,7 @@ function moveOptionDown(index: number) {
123148
:label="`Option #${index + 1}`"
124149
placeholder="Lorem Ipsum"
125150
class="flex-1"
151+
required
126152
/>
127153
<Button
128154
color="danger"

frontend/src/views/Verwaltung/Veranstaltungen/CustomFields/CustomFieldVeranstaltungCreate.vue

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ const form = ref<ICustomFieldData>({
1919
required: false,
2020
type: 'BasicInput',
2121
options: [],
22+
positions: [],
2223
})
2324
2425
const validationErrors = ref([])
@@ -36,6 +37,7 @@ const { execute, error, isLoading } = useAsyncState(
3637
required: data.required,
3738
type: data.type,
3839
options: data.options || [],
40+
positions: data.positions || [],
3941
},
4042
})
4143

frontend/src/views/Verwaltung/Veranstaltungen/CustomFields/CustomFieldVeranstaltungEdit.vue

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,15 +28,17 @@ watch(field, () => {
2828
required: field.value?.required ?? false,
2929
type: (field.value?.type ?? 'BasicInput') as CustomFieldType,
3030
options: field.value?.options ?? [],
31+
positions: field.value?.positions ?? [],
3132
}
3233
})
3334
3435
const form = ref<ICustomFieldData>({
3536
name: '',
3637
description: '',
3738
required: false,
38-
options: [],
3939
type: 'BasicInput',
40+
options: [],
41+
positions: [],
4042
})
4143
4244
const validationErrors = ref([])
@@ -58,6 +60,7 @@ const {
5860
required: data.required,
5961
type: data.type,
6062
options: data.options,
63+
positions: data.positions,
6164
},
6265
})
6366

0 commit comments

Comments
 (0)