diff --git a/apps/api/prisma/migrations/20260304182124_custom_field_order/migration.sql b/apps/api/prisma/migrations/20260304182124_custom_field_order/migration.sql
new file mode 100644
index 00000000..79c1d2b8
--- /dev/null
+++ b/apps/api/prisma/migrations/20260304182124_custom_field_order/migration.sql
@@ -0,0 +1,2 @@
+-- AlterTable
+ALTER TABLE "CustomField" ADD COLUMN "order" INTEGER;
diff --git a/apps/api/prisma/schema/CustomField.prisma b/apps/api/prisma/schema/CustomField.prisma
index 0a1415df..ac05f0a7 100644
--- a/apps/api/prisma/schema/CustomField.prisma
+++ b/apps/api/prisma/schema/CustomField.prisma
@@ -19,6 +19,7 @@ model CustomField {
id String @id @default(uuid(7))
name String
description String?
+ order Int?
type CustomFieldType
required Boolean @default(false)
options String[]
diff --git a/apps/api/src/services/customFields/customFields.router.ts b/apps/api/src/services/customFields/customFields.router.ts
index 3e38d8cc..5f861fa8 100644
--- a/apps/api/src/services/customFields/customFields.router.ts
+++ b/apps/api/src/services/customFields/customFields.router.ts
@@ -9,6 +9,8 @@ import { customFieldsVeranstaltungCreate } from './customFieldsVeranstaltungCrea
import { customFieldsVeranstaltungDelete, customFieldsUnterveranstaltungDelete } from './customFieldsDelete.js'
import { customFieldsUpdate } from './customFieldsUpdate.js'
import { customFieldValuesUpdate } from './customFieldValuesUpdate.js'
+import { customFieldUnterveranstaltungOrder } from './schema/customFieldsUnterveranstaltungOrder.js'
+import { customFieldVeranstaltungOrder } from './schema/customFieldsVeranstaltungOrder.js'
// Import Routes here - do not delete this line
export const customFieldsRouter = mergeRouters(
@@ -21,6 +23,8 @@ export const customFieldsRouter = mergeRouters(
customFieldsUnterveranstaltungDelete,
customFieldsUnterveranstaltungCreate,
customFieldValuesUpdate,
- customFieldsTemplates
+ customFieldsTemplates,
+ customFieldVeranstaltungOrder,
+ customFieldUnterveranstaltungOrder
// Add Routes here - do not delete this line
)
diff --git a/apps/api/src/services/customFields/customFieldsList.ts b/apps/api/src/services/customFields/customFieldsList.ts
index f7d9a09c..35fbd0f9 100644
--- a/apps/api/src/services/customFields/customFieldsList.ts
+++ b/apps/api/src/services/customFields/customFieldsList.ts
@@ -2,7 +2,7 @@ import { z } from 'zod'
import { CustomFieldPosition, CustomFieldType, Prisma } from '@prisma/client'
import prisma from '../../prisma.js'
-import { definePublicQueryProcedure } from '../../types/defineProcedure.js'
+import { defineProtectedQueryProcedure, definePublicQueryProcedure } from '../../types/defineProcedure.js'
import {
calculatePagination,
defineEmptyQueryResponse,
@@ -10,6 +10,7 @@ import {
defineTableInput,
} from '../../types/defineTableProcedure.js'
import { boolish } from '../../util/zod.js'
+import { getGliederungRequireAdmin } from '../../util/getGliederungRequireAdmin.js'
const baseFilter = z.strictObject({
entity: z.enum(['veranstaltung', 'unterveranstaltung']),
@@ -23,6 +24,7 @@ export const customFieldsTable = definePublicQueryProcedure({
async handler({ input: { entity, entityId, position } }) {
if (entity === 'veranstaltung') {
return await prisma.customField.findMany({
+ orderBy: [{ order: { sort: 'asc', nulls: 'last' } }],
where: {
veranstaltungId: entityId,
positions:
@@ -35,6 +37,14 @@ export const customFieldsTable = definePublicQueryProcedure({
})
} else if (entity === 'unterveranstaltung') {
return await prisma.customField.findMany({
+ orderBy: [
+ {
+ unterveranstaltungId: { sort: 'asc', nulls: 'first' },
+ },
+ {
+ order: { sort: 'asc', nulls: 'last' },
+ },
+ ],
where: {
positions:
position === undefined
@@ -64,8 +74,9 @@ export const customFieldsTable = definePublicQueryProcedure({
},
})
-export const customFieldsList = definePublicQueryProcedure({
+export const customFieldsList = defineProtectedQueryProcedure({
key: 'table',
+ roleIds: ['ADMIN', 'GLIEDERUNG_ADMIN'],
inputSchema: baseFilter.extend({
table: defineTableInput({
filter: {
@@ -74,10 +85,14 @@ export const customFieldsList = definePublicQueryProcedure({
required: boolish,
position: z.nativeEnum(CustomFieldPosition),
},
- orderBy: ['name'],
+ orderBy: ['name', 'order'],
}),
}),
- async handler({ input }) {
+ async handler({ ctx, input }) {
+ if (ctx.account.role === 'GLIEDERUNG_ADMIN') {
+ await getGliederungRequireAdmin(ctx.accountId)
+ }
+
const where: Prisma.CustomFieldWhereInput = {
name: {
contains: input.table?.filter?.name,
@@ -93,6 +108,25 @@ export const customFieldsList = definePublicQueryProcedure({
},
}
+ if (input.entity === 'veranstaltung') {
+ where.veranstaltungId = input.entityId
+ } else if (input.entity === 'unterveranstaltung') {
+ where.OR = [
+ {
+ unterveranstaltungId: input.entityId,
+ },
+ {
+ veranstaltung: {
+ unterveranstaltungen: {
+ some: {
+ id: input.entityId,
+ },
+ },
+ },
+ },
+ ]
+ }
+
const total = await prisma.customField.count({ where })
const { pageIndex, pageSize, pages } = calculatePagination(total, input.table?.pagination)
@@ -100,15 +134,13 @@ export const customFieldsList = definePublicQueryProcedure({
const customFields = await prisma.customField.findMany({
take: pageSize,
skip: pageSize * pageIndex,
- where: {
- ...where,
- veranstaltungId: input.entityId,
- },
+ where,
orderBy: input.table?.orderBy,
select: {
id: true,
name: true,
description: true,
+ order: true,
type: true,
positions: true,
required: true,
@@ -122,27 +154,18 @@ export const customFieldsList = definePublicQueryProcedure({
const customFields = await prisma.customField.findMany({
take: pageSize,
skip: pageSize * pageIndex,
- where: {
- ...where,
- OR: [
- {
- unterveranstaltungId: input.entityId,
- },
- {
- veranstaltung: {
- unterveranstaltungen: {
- some: {
- id: input.entityId,
- },
- },
- },
- },
- ],
- },
+ orderBy: [
+ {
+ unterveranstaltungId: { sort: 'asc', nulls: 'first' },
+ },
+ ...(input.table?.orderBy ?? []),
+ ],
+ where,
select: {
id: true,
name: true,
description: true,
+ order: true,
type: true,
positions: true,
required: true,
diff --git a/apps/api/src/services/customFields/schema/customField.schema.ts b/apps/api/src/services/customFields/schema/customField.schema.ts
index 8172e451..af4585c1 100644
--- a/apps/api/src/services/customFields/schema/customField.schema.ts
+++ b/apps/api/src/services/customFields/schema/customField.schema.ts
@@ -1,11 +1,21 @@
import { CustomFieldPosition, CustomFieldType } from '@prisma/client'
import { z } from 'zod'
-export const customFieldSchema = z.strictObject({
- name: z.string().min(1),
- description: z.string().nullable(),
- type: z.nativeEnum(CustomFieldType),
- required: z.boolean(),
- options: z.array(z.string()),
- positions: z.nativeEnum(CustomFieldPosition).array(),
-})
+export const customFieldSchema = z
+ .strictObject({
+ name: z.string().min(1),
+ description: z.string().nullable(),
+ type: z.nativeEnum(CustomFieldType),
+ required: z.boolean(),
+ options: z.array(z.string()),
+ positions: z.nativeEnum(CustomFieldPosition).array().min(1),
+ })
+ .superRefine((values, ctx) => {
+ const optionTypes = ['BASIC_DROPDOWN', 'BASIC_RADIO', 'BASIC_SWITCH'] as CustomFieldType[]
+ if (optionTypes.includes(values.type) && values.options.length === 0) {
+ ctx.addIssue({
+ code: 'custom',
+ message: 'Es muss mindestens eine Auswahlmöglichkeit angegeben werden',
+ })
+ }
+ })
diff --git a/apps/api/src/services/customFields/schema/customFieldsUnterveranstaltungOrder.ts b/apps/api/src/services/customFields/schema/customFieldsUnterveranstaltungOrder.ts
new file mode 100644
index 00000000..2b0f974a
--- /dev/null
+++ b/apps/api/src/services/customFields/schema/customFieldsUnterveranstaltungOrder.ts
@@ -0,0 +1,54 @@
+import z from 'zod'
+import { defineProtectedMutateProcedure } from '../../../types/defineProcedure.js'
+import { getGliederungRequireAdmin } from '../../../util/getGliederungRequireAdmin.js'
+import { TRPCError } from '@trpc/server'
+import prisma from '../../../prisma.js'
+
+export const customFieldUnterveranstaltungOrder = defineProtectedMutateProcedure({
+ key: 'unterveranstaltungOrder',
+ roleIds: ['ADMIN', 'GLIEDERUNG_ADMIN'],
+ inputSchema: z.strictObject({
+ unterveranstaltungId: z.string().uuid(),
+ fields: z.array(z.string().uuid()),
+ }),
+ handler: async ({ ctx, input }) => {
+ if (ctx.account.role === 'GLIEDERUNG_ADMIN') {
+ const gliederung = await getGliederungRequireAdmin(ctx.accountId)
+ if (gliederung.id !== input.unterveranstaltungId) {
+ throw new TRPCError({
+ code: 'FORBIDDEN',
+ })
+ }
+ }
+
+ const fields = await prisma.customField.findMany({
+ where: {
+ unterveranstaltungId: input.unterveranstaltungId,
+ id: {
+ in: input.fields,
+ },
+ },
+ })
+
+ if (fields.length !== input.fields.length) {
+ throw new TRPCError({
+ code: 'BAD_REQUEST',
+ message: 'Some supplied fields do not belong to the given unterveranstaltung!',
+ })
+ }
+
+ await prisma.$transaction(
+ input.fields.map((field, index) =>
+ prisma.customField.update({
+ where: {
+ unterveranstaltungId: input.unterveranstaltungId,
+ id: field,
+ },
+ data: {
+ order: index + 1,
+ },
+ })
+ )
+ )
+ },
+})
diff --git a/apps/api/src/services/customFields/schema/customFieldsVeranstaltungOrder.ts b/apps/api/src/services/customFields/schema/customFieldsVeranstaltungOrder.ts
new file mode 100644
index 00000000..3cc63732
--- /dev/null
+++ b/apps/api/src/services/customFields/schema/customFieldsVeranstaltungOrder.ts
@@ -0,0 +1,44 @@
+import { TRPCError } from '@trpc/server'
+import z from 'zod'
+import prisma from '../../../prisma.js'
+import { defineProtectedMutateProcedure } from '../../../types/defineProcedure.js'
+
+export const customFieldVeranstaltungOrder = defineProtectedMutateProcedure({
+ key: 'veranstaltungOrder',
+ roleIds: ['ADMIN'],
+ inputSchema: z.strictObject({
+ veranstaltungId: z.string().uuid(),
+ fields: z.array(z.string().uuid()),
+ }),
+ handler: async ({ input }) => {
+ const fields = await prisma.customField.findMany({
+ where: {
+ veranstaltungId: input.veranstaltungId,
+ id: {
+ in: input.fields,
+ },
+ },
+ })
+
+ if (fields.length !== input.fields.length) {
+ throw new TRPCError({
+ code: 'BAD_REQUEST',
+ message: 'Some supplied fields do not belong to the given veranstaltung!',
+ })
+ }
+
+ await prisma.$transaction(
+ input.fields.map((field, index) =>
+ prisma.customField.update({
+ where: {
+ veranstaltungId: input.veranstaltungId,
+ id: field,
+ },
+ data: {
+ order: index + 1,
+ },
+ })
+ )
+ )
+ },
+})
diff --git a/apps/frontend/src/components/CustomFields/CustomFieldsTable.vue b/apps/frontend/src/components/CustomFields/CustomFieldsTable.vue
index f9bcf945..2ec38a34 100644
--- a/apps/frontend/src/components/CustomFields/CustomFieldsTable.vue
+++ b/apps/frontend/src/components/CustomFields/CustomFieldsTable.vue
@@ -32,6 +32,18 @@ const router = useRouter()
const column = createColumnHelper
+ Hier kann die Reihenfolge der benutzerdefinierten Felder deiner Ausschreibung verändert werden.
+
+ Hinweis: Die Spalte Reihenfolge gliedert sich wie folgt: Die erste Zahl zeigt die aktuelle Position in
+ der Reihenfolge wohingegen die Zahl in der Klammer die neue Position anzeigt.
+ Deine Änderungen wurden erfolgreich gespeichert.
+
+
+
+
+
+
+
+
+ Reihenfolge
+ Name
+ Typ
+ Quelle
+
+
+
+
+
+
+ {{ field.name }}
+ {{ formatType(field) }}
+ {{ formatSource(field) }}
+
+ Hier kann die Reihenfolge benutzerdefinierter Felder der Veranstaltung verändert werden. +
+| Reihenfolge | +Name | +Typ | +Quelle | +
|---|---|---|---|
|
+
+ {{ field.order || '-' }}
+ ( {{ index + 1 }} )
+
+
+
+
+
+ |
+ {{ field.name }} | +{{ formatType(field) }} | +Veranstaltung | +
+ Hinweis: Die Spalte Reihenfolge gliedert sich wie folgt: Die erste Zahl zeigt die aktuelle Position in + der Reihenfolge wohingegen die Zahl in der Klammer die neue Position anzeigt. +
+ +Deine Änderungen wurden erfolgreich gespeichert.
+ +