Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Veranstaltung" ADD COLUMN "settings" JSONB NOT NULL DEFAULT '{}';
1 change: 1 addition & 0 deletions apps/api/prisma/schema/Veranstaltung.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,5 @@ model Veranstaltung {
hostnameId Int?
hostname Hostname? @relation(fields: [hostnameId], references: [id])
customFields CustomField[]
settings Json @default("{}")
}
2 changes: 2 additions & 0 deletions apps/api/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,5 @@ export type RouterOutput = inferRouterOutputs<AppRouter>

export * from './types/enums/index.js'
export * from './types/enums/mappings/index.js'

export type { VeranstaltungSettings } from './services/veranstaltung/veranstaltung.schema.js'
6 changes: 5 additions & 1 deletion apps/api/src/services/anmeldung/anmeldungPublicCreate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import logActivity from '../../util/activity.js'
import { sendMail } from '../../util/mail.js'
import { getPersonCreateData, personSchema } from '../person/schema/person.schema.js'
import { updateMeiliPerson } from '../../meilisearch/person.js'
import { veranstaltungSettingsGet } from '../veranstaltung/veranstaltung.schema.js'

export const inputSchema = z.strictObject({
token: z.string().optional(),
Expand Down Expand Up @@ -46,6 +47,7 @@ export const anmeldungPublicCreateProcedure = definePublicMutateProcedure({
},
},
maxTeilnehmende: true,
settings: true,
unterveranstaltungen: {
select: {
_count: {
Expand Down Expand Up @@ -141,7 +143,9 @@ export const anmeldungPublicCreateProcedure = definePublicMutateProcedure({
gliederung: person.gliederung,
})

const accessToken = randomUUID()
const accessToken = veranstaltungSettingsGet(unterveranstaltung.veranstaltung.settings, 'enablePhotoUpload')
? randomUUID()
: null
const assignmentCode = ctx.authenticated ? null : randomUUID()
const anmeldung = await prisma.anmeldung.create({
data: {
Expand Down
7 changes: 6 additions & 1 deletion apps/api/src/services/veranstaltung/veranstaltung.router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
import { mergeRouters } from '../../trpc.js'

import { veranstaltungGliederungListProcedure } from './veranstaltungGliederungList.js'
import { veranstaltungSettingsGetProcedure } from './veranstaltungSettingsGet.js'
import { veranstaltungSettingsPatchProcedure } from './veranstaltungSettingsPatch.js'
import { veranstaltungVerwaltungCreateProcedure } from './veranstaltungVerwaltungCreate.js'
import { veranstaltungVerwaltungGetProcedure } from './veranstaltungVerwaltungGet.js'
import {
Expand All @@ -17,6 +19,9 @@ export const veranstaltungRouter = mergeRouters(
veranstaltungVerwaltungListProcedure,
veranstaltungVerwaltungCountProcedure,
veranstaltungVerwaltungPatchProcedure,
veranstaltungGliederungListProcedure
veranstaltungGliederungListProcedure,

veranstaltungSettingsGetProcedure,
veranstaltungSettingsPatchProcedure
// Add Routes here - do not delete this line
)
31 changes: 31 additions & 0 deletions apps/api/src/services/veranstaltung/veranstaltung.schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { z } from 'zod'
import client from '../../prisma.js'
import type { JsonValue } from '@prisma/client/runtime/library'

export const veranstaltungSettingsSchema = z.strictObject({
enablePhotoUpload: z.boolean(),
})

export type VeranstaltungSettings = z.infer<typeof veranstaltungSettingsSchema>

export const veranstaltungSettingsDefaults: VeranstaltungSettings = {
enablePhotoUpload: false,
}

export async function populateVeranstaltungSettings(veranstaltungId: number) {
await client.veranstaltung.update({
where: {
id: veranstaltungId,
},
data: {
settings: veranstaltungSettingsDefaults,
},
})
}

export function veranstaltungSettingsGet<K extends keyof VeranstaltungSettings>(
raw: JsonValue,
key: K
): VeranstaltungSettings[K] {
return (raw as VeranstaltungSettings)[key]
}
24 changes: 24 additions & 0 deletions apps/api/src/services/veranstaltung/veranstaltungSettingsGet.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { z } from 'zod'
import { defineProtectedQueryProcedure } from '../../types/defineProcedure.js'
import client from '../../prisma.js'
import type { VeranstaltungSettings } from './veranstaltung.schema.js'

export const veranstaltungSettingsGetProcedure = defineProtectedQueryProcedure({
key: 'settingsGet',
roleIds: ['ADMIN'],
inputSchema: z.strictObject({
veranstaltungId: z.number().int(),
}),
handler: async ({ input }) => {
const { settings } = await client.veranstaltung.findFirstOrThrow({
where: {
id: input.veranstaltungId,
},
select: {
settings: true,
},
})

return settings as VeranstaltungSettings
},
})
23 changes: 23 additions & 0 deletions apps/api/src/services/veranstaltung/veranstaltungSettingsPatch.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { z } from 'zod'
import client from '../../prisma.js'
import { defineProtectedMutateProcedure } from '../../types/defineProcedure.js'
import { veranstaltungSettingsSchema } from './veranstaltung.schema.js'

export const veranstaltungSettingsPatchProcedure = defineProtectedMutateProcedure({
key: 'settingsPatch',
roleIds: ['ADMIN'],
inputSchema: z.strictObject({
veranstaltungId: z.number().int(),
settings: veranstaltungSettingsSchema,
}),
handler: async ({ input }) => {
await client.veranstaltung.update({
where: {
id: input.veranstaltungId,
},
data: {
settings: input.settings,
},
})
},
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
<script setup lang="ts">
import { apiClient } from '@/api'
import BasicSwitch from '@/components/BasicInputs/BasicSwitch.vue'
import Button from '@/components/UIComponents/Button.vue'
import { type VeranstaltungSettings } from '@codeanker/api'
import { ValidateForm } from '@codeanker/validation'
import { useAsyncState } from '@vueuse/core'
import { ref, watch } from 'vue'
import { toast } from 'vue-sonner'

const { veranstaltungId } = defineProps<{
veranstaltungId: number
}>()

const { state: settings, execute } = useAsyncState(
() =>
apiClient.veranstaltung.settingsGet.query({
veranstaltungId,
}),
null,
{ immediate: true }
)

const settingsCopy = ref<VeranstaltungSettings>({ enablePhotoUpload: false })

watch(settings, (settings) => {
if (settings) {
settingsCopy.value = settings
}
})

const errorCreate = ref<Error>()

async function submit() {
try {
await apiClient.veranstaltung.settingsPatch.mutate({
veranstaltungId,
settings: settingsCopy.value,
})
await execute()
toast('Einstellungen gespeichert')
} catch (e) {
toast.error(`${e}`)
}
}
</script>

<template>
<ValidateForm
v-if="settings"
@submit="submit"
>
<div class="space-y-2">
<BasicSwitch
v-model="settingsCopy.enablePhotoUpload"
label="Fotoupload aktivieren"
/>
<span class="text-sm text-gray-600 dark:text-gray-400">
Über den Fotoupload können Teilnehmende eigenständig ein Profilbild hochladen, welches dann bspw. für Ausweise
verwendet werden kann.
</span>
</div>

<div class="mt-8 flex gap-4">
<Button
type="submit"
color="primary"
>
Speichern
</Button>
</div>

<div
v-if="errorCreate"
class="bg-danger-400 mb-2 mt-5 rounded p-3 text-center text-white"
>
{{ errorCreate }}
</div>
</ValidateForm>
</template>
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import {
CameraIcon,
ClipboardDocumentListIcon,
CogIcon,
DocumentIcon,
MegaphoneIcon,
SquaresPlusIcon,
Expand All @@ -24,6 +25,7 @@
import { useRouteTitle } from '@/composables/useRouteTitle'
import { formatDateWith } from '@codeanker/helpers'
import { PlusIcon } from '@heroicons/vue/24/solid'
import FormVeranstaltungSettings from '@/components/forms/veranstaltung/FormVeranstaltungSettings.vue'

const { setTitle } = useRouteTitle()

Expand Down Expand Up @@ -76,6 +78,7 @@
{ name: 'Bedingungen', icon: ClipboardDocumentListIcon },
{ name: 'Ausschreibungen', icon: MegaphoneIcon },
{ name: 'Felder', icon: SquaresPlusIcon },
{ name: 'Einstellungen', icon: CogIcon },
]
return tabs
})
Expand Down Expand Up @@ -170,7 +173,7 @@
<div
v-if="veranstaltung?.teilnahmeBedingungenPublic"
class="prose dark:prose-invert"
v-html="veranstaltung?.teilnahmeBedingungenPublic"

Check warning on line 176 in apps/frontend/src/views/Verwaltung/Veranstaltungen/VeranstaltungDetail.vue

View workflow job for this annotation

GitHub Actions / 🔍 turbo-checks

'v-html' directive can lead to XSS attack
/>
<div v-else>
<p class="text-gray-500">Keine öffentlichen Teilnahmebedingungen hinterlegt</p>
Expand All @@ -183,7 +186,7 @@
<div
v-if="veranstaltung?.teilnahmeBedingungen"
class="prose dark:prose-invert"
v-html="veranstaltung?.teilnahmeBedingungen"

Check warning on line 189 in apps/frontend/src/views/Verwaltung/Veranstaltungen/VeranstaltungDetail.vue

View workflow job for this annotation

GitHub Actions / 🔍 turbo-checks

'v-html' directive can lead to XSS attack
/>
<div v-else>
<p class="text-gray-500">Keine internen Teilnahmebedingungen hinterlegt</p>
Expand All @@ -196,7 +199,7 @@
<div
v-if="veranstaltung?.datenschutz"
class="prose dark:prose-invert"
v-html="veranstaltung?.datenschutz"

Check warning on line 202 in apps/frontend/src/views/Verwaltung/Veranstaltungen/VeranstaltungDetail.vue

View workflow job for this annotation

GitHub Actions / 🔍 turbo-checks

'v-html' directive can lead to XSS attack
/>
<div v-else>
<p class="text-gray-500">Keine Datenschutzhinweise hinterlegt</p>
Expand Down Expand Up @@ -245,5 +248,16 @@
entity="veranstaltung"
/>
</Tab>
<Tab>
<div class="my-5 lg:mt-10">
<div class="text-lg font-semibold">Einstellungen</div>
<p class="max-w-2xl text-sm text-gray-500">Hier können veranstaltungsweite Eintellungen getroffen werden.</p>
</div>

<FormVeranstaltungSettings
v-if="veranstaltung"
:veranstaltung-id="veranstaltung.id"
/>
</Tab>
</Tabs>
</template>
Loading