diff --git a/apps/api/email/registration-successful.mjml b/apps/api/email/registration-successful.mjml index 1e49badc..f61ffa83 100644 --- a/apps/api/email/registration-successful.mjml +++ b/apps/api/email/registration-successful.mjml @@ -18,6 +18,13 @@ >Hier Foto hochladen + + + Du kannst den Fotoupload beliebig oft machen, bis der Meldeschluss erreicht ist. + {{/if}} - prisma.anmeldung.findFirst({ + handler: ({ input: { unterveranstaltungId, anmeldungId, accessToken } }) => { + return prisma.anmeldung.findFirst({ where: { unterveranstaltungId, id: anmeldungId, @@ -23,6 +23,7 @@ export const anmeldungAccessTokenValidateProcedure = definePublicQueryProcedure( id: true, firstname: true, lastname: true, + photoId: true, }, }, unterveranstaltung: { @@ -40,5 +41,6 @@ export const anmeldungAccessTokenValidateProcedure = definePublicQueryProcedure( }, }, }, - }), + }) + }, }) diff --git a/apps/api/src/services/anmeldung/anmeldungFotoUpload.ts b/apps/api/src/services/anmeldung/anmeldungFotoUpload.ts index 240d3a61..c68fc2f1 100644 --- a/apps/api/src/services/anmeldung/anmeldungFotoUpload.ts +++ b/apps/api/src/services/anmeldung/anmeldungFotoUpload.ts @@ -3,6 +3,8 @@ import z from 'zod' import prisma from '../../prisma.js' import { definePublicMutateProcedure } from '../../types/defineProcedure.js' +import { TRPCError } from '@trpc/server' +import type { Prisma } from '@prisma/client' export const anmeldungFotoUploadProcedure = definePublicMutateProcedure({ key: 'anmeldungFotoUpload', @@ -13,14 +15,43 @@ export const anmeldungFotoUploadProcedure = definePublicMutateProcedure({ fileId: z.string(), }), handler: async ({ input: { unterveranstaltungId, anmeldungId, accessToken, fileId } }) => { - await prisma.anmeldung.update({ + const unterveranstaltung = await prisma.unterveranstaltung.findFirst({ where: { - unterveranstaltungId, - id: anmeldungId, - accessToken, + id: unterveranstaltungId, + meldeschluss: { + gt: new Date(), + }, }, + }) + if (unterveranstaltung === null) { + throw new TRPCError({ + code: 'FORBIDDEN', + message: 'Der Meldeschluss ist bereits erreicht!', + }) + } + + const where: Prisma.AnmeldungWhereUniqueInput = { + unterveranstaltungId, + id: anmeldungId, + accessToken, + } + const anmeldung = await prisma.anmeldung.findUniqueOrThrow({ + where, + select: { + person: { + select: { + photoId: true, + }, + }, + }, + }) + if (anmeldung.person.photoId !== null) { + // TODO: Delete old file + } + + await prisma.anmeldung.update({ + where, data: { - accessToken: null, person: { update: { photoId: fileId, diff --git a/apps/api/src/services/customFields/customFields.router.ts b/apps/api/src/services/customFields/customFields.router.ts index f7d75c3d..b3d6b8d6 100644 --- a/apps/api/src/services/customFields/customFields.router.ts +++ b/apps/api/src/services/customFields/customFields.router.ts @@ -6,8 +6,8 @@ import { customFieldsList } from './customFieldsList.js' import { customFieldsTemplates } from './customFieldsTemplates.js' import { customFieldsUnterveranstaltungCreate } from './customFieldsUnterveranstaltungCreate.js' import { customFieldsVeranstaltungCreate } from './customFieldsVeranstaltungCreate.js' -import { customFieldsVeranstaltungDelete } from './customFieldsVeranstaltungDelete.js' -import { customFieldsVeranstaltungUpdate } from './customFieldsVeranstaltungUpdate.js' +import { customFieldsDelete } from './customFieldsDelete.js' +import { customFieldsUpdate } from './customFieldsUpdate.js' import { customFieldValuesUpdate } from './customFieldValuesUpdate.js' // Import Routes here - do not delete this line @@ -15,8 +15,8 @@ export const customFieldsRouter = mergeRouters( customFieldsList.router, customFieldsGet.router, customFieldsVeranstaltungCreate.router, - customFieldsVeranstaltungUpdate.router, - customFieldsVeranstaltungDelete.router, + customFieldsUpdate.router, + customFieldsDelete.router, customFieldsUnterveranstaltungCreate.router, customFieldValuesUpdate.router, customFieldsTemplates.router diff --git a/apps/api/src/services/customFields/customFieldsDelete.ts b/apps/api/src/services/customFields/customFieldsDelete.ts new file mode 100644 index 00000000..8f7adccc --- /dev/null +++ b/apps/api/src/services/customFields/customFieldsDelete.ts @@ -0,0 +1,66 @@ +import { Role } from '@prisma/client' +import { TRPCError } from '@trpc/server' +import { z } from 'zod' + +import prisma from '../../prisma.js' +import { defineProtectedMutateProcedure } from '../../types/defineProcedure.js' + +export const customFieldsDelete = defineProtectedMutateProcedure({ + key: 'delete', + roleIds: [Role.ADMIN, Role.GLIEDERUNG_ADMIN], + inputSchema: z.strictObject({ + veranstaltungId: z.number(), + fieldId: z.number(), + }), + async handler({ ctx, input }) { + if (ctx.account.role === Role.GLIEDERUNG_ADMIN) { + const { gliederungId: gliederungIdActor } = await prisma.person.findFirstOrThrow({ + where: { + account: { + id: ctx.accountId, + }, + }, + select: { + gliederungId: true, + }, + }) + const customField = await prisma.customField.findUniqueOrThrow({ + where: { + id: input.fieldId, + }, + select: { + unterveranstaltung: { + select: { + gliederungId: true, + }, + }, + }, + }) + if (gliederungIdActor !== customField.unterveranstaltung?.gliederungId) { + throw new TRPCError({ + code: 'NOT_FOUND', + }) + } + } + + const field = await prisma.customField.findUnique({ + where: { + id: input.fieldId, + veranstaltungId: input.veranstaltungId, + }, + }) + + if (field === null) { + throw new TRPCError({ + code: 'NOT_FOUND', + }) + } + + await prisma.customField.delete({ + where: { + id: input.fieldId, + veranstaltungId: input.veranstaltungId, + }, + }) + }, +}) diff --git a/apps/api/src/services/customFields/customFieldsUpdate.ts b/apps/api/src/services/customFields/customFieldsUpdate.ts new file mode 100644 index 00000000..4ef8c3ea --- /dev/null +++ b/apps/api/src/services/customFields/customFieldsUpdate.ts @@ -0,0 +1,57 @@ +import { Role } from '@prisma/client' +import { z } from 'zod' + +import prisma from '../../prisma.js' +import { defineProtectedMutateProcedure } from '../../types/defineProcedure.js' + +import { TRPCError } from '@trpc/server' +import { customFieldSchema } from './schema/customField.schema.js' + +export const customFieldsUpdate = defineProtectedMutateProcedure({ + key: 'update', + roleIds: [Role.ADMIN, Role.GLIEDERUNG_ADMIN], + inputSchema: z.strictObject({ + fieldId: z.number(), + data: customFieldSchema, + }), + async handler({ ctx, input }) { + if (ctx.account.role === Role.GLIEDERUNG_ADMIN) { + const { gliederungId: gliederungIdActor } = await prisma.person.findFirstOrThrow({ + where: { + account: { + id: ctx.accountId, + }, + }, + select: { + gliederungId: true, + }, + }) + const customField = await prisma.customField.findUniqueOrThrow({ + where: { + id: input.fieldId, + }, + select: { + unterveranstaltung: { + select: { + gliederungId: true, + }, + }, + }, + }) + if (gliederungIdActor !== customField.unterveranstaltung?.gliederungId) { + throw new TRPCError({ + code: 'NOT_FOUND', + }) + } + } + + return await prisma.customField.update({ + where: { + id: input.fieldId, + }, + data: { + ...input.data, + }, + }) + }, +}) diff --git a/apps/api/src/services/customFields/customFieldsVeranstaltungDelete.ts b/apps/api/src/services/customFields/customFieldsVeranstaltungDelete.ts deleted file mode 100644 index f6876733..00000000 --- a/apps/api/src/services/customFields/customFieldsVeranstaltungDelete.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { Role } from '@prisma/client' -import { TRPCError } from '@trpc/server' -import { z } from 'zod' - -import prisma from '../../prisma.js' -import { defineProtectedMutateProcedure } from '../../types/defineProcedure.js' - -export const customFieldsVeranstaltungDelete = defineProtectedMutateProcedure({ - key: 'veranstaltungDelete', - roleIds: [Role.ADMIN], - inputSchema: z.strictObject({ - veranstaltungId: z.number(), - fieldId: z.number(), - }), - async handler({ input }) { - const field = await prisma.customField.findUnique({ - where: { - id: input.fieldId, - veranstaltungId: input.veranstaltungId, - }, - }) - - if (field === null) { - throw new TRPCError({ - code: 'NOT_FOUND', - }) - } - - await prisma.customField.delete({ - where: { - id: input.fieldId, - veranstaltungId: input.veranstaltungId, - }, - }) - }, -}) diff --git a/apps/api/src/services/customFields/customFieldsVeranstaltungUpdate.ts b/apps/api/src/services/customFields/customFieldsVeranstaltungUpdate.ts deleted file mode 100644 index d8e0fd73..00000000 --- a/apps/api/src/services/customFields/customFieldsVeranstaltungUpdate.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { Role } from '@prisma/client' -import { z } from 'zod' - -import prisma from '../../prisma.js' -import { defineProtectedMutateProcedure } from '../../types/defineProcedure.js' - -import { customFieldSchema } from './schema/customField.schema.js' - -export const customFieldsVeranstaltungUpdate = defineProtectedMutateProcedure({ - key: 'veranstaltungUpdate', - roleIds: [Role.ADMIN], - inputSchema: z.strictObject({ - fieldId: z.number(), - data: customFieldSchema, - }), - async handler({ input }) { - return await prisma.customField.update({ - where: { - id: input.fieldId, - }, - data: { - ...input.data, - }, - }) - }, -}) diff --git a/apps/api/src/services/person/person.router.ts b/apps/api/src/services/person/person.router.ts index 31f0aace..6ae0554a 100644 --- a/apps/api/src/services/person/person.router.ts +++ b/apps/api/src/services/person/person.router.ts @@ -2,10 +2,9 @@ import { mergeRouters } from '../../trpc.js' import { personAuthenticatedGetProcedure } from './personAuthenticatedGet.js' import { personGetProcedure } from './personGet.js' -import { personGliederungPatchProcedure } from './personGliederungPatch.js' import { personCountProcedure, personListProcedure } from './personList.js' import { personVerwaltungCreateProcedure } from './personVerwaltungCreate.js' -import { personVerwaltungPatchProcedure } from './personVerwaltungPatch.js' +import { personVerwaltungPatchProcedure } from './personPatch.js' import { personVerwaltungRemoveProcedure } from './personVerwaltungRemove.js' // Import Routes here - do not delete this line @@ -16,7 +15,6 @@ export const personRouter = mergeRouters( personCountProcedure.router, personGetProcedure.router, personVerwaltungPatchProcedure.router, - personVerwaltungRemoveProcedure.router, - personGliederungPatchProcedure.router + personVerwaltungRemoveProcedure.router // Add Routes here - do not delete this line ) diff --git a/apps/api/src/services/person/personGet.ts b/apps/api/src/services/person/personGet.ts index 11a645d0..a4f77bb0 100644 --- a/apps/api/src/services/person/personGet.ts +++ b/apps/api/src/services/person/personGet.ts @@ -17,8 +17,6 @@ export const personGetProcedure = defineProtectedQueryProcedure({ id: input.id, } - console.log(where) - return prisma.person.findUniqueOrThrow({ where, select: { diff --git a/apps/api/src/services/person/personGliederungPatch.ts b/apps/api/src/services/person/personGliederungPatch.ts deleted file mode 100644 index 11f669de..00000000 --- a/apps/api/src/services/person/personGliederungPatch.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { Role } from '@prisma/client' -import z from 'zod' - -import prisma from '../../prisma.js' -import { defineProtectedMutateProcedure } from '../../types/defineProcedure.js' - -import { getPersonCreateData, personSchema } from './schema/person.schema.js' - -export const personGliederungPatchProcedure = defineProtectedMutateProcedure({ - key: 'gliederungPatch', - roleIds: [Role.GLIEDERUNG_ADMIN], - inputSchema: z.strictObject({ - id: z.number().int(), - data: personSchema, - }), - async handler({ input }) { - await prisma.notfallkontakt.deleteMany({ - where: { - personId: input.id, - }, - }) - - return prisma.person.update({ - where: { - id: input.id, - }, - data: await getPersonCreateData(input.data), - select: { - id: true, - }, - }) - }, -}) diff --git a/apps/api/src/services/person/personPatch.ts b/apps/api/src/services/person/personPatch.ts new file mode 100644 index 00000000..6e690ce6 --- /dev/null +++ b/apps/api/src/services/person/personPatch.ts @@ -0,0 +1,61 @@ +import { Role } from '@prisma/client' +import z from 'zod' + +import prisma from '../../prisma.js' +import { defineProtectedMutateProcedure } from '../../types/defineProcedure.js' + +import { getPersonCreateData, personSchemaOptional } from './schema/person.schema.js' +import { TRPCError } from '@trpc/server' + +export const personVerwaltungPatchProcedure = defineProtectedMutateProcedure({ + key: 'patch', + roleIds: [Role.ADMIN, Role.GLIEDERUNG_ADMIN], + inputSchema: z.strictObject({ + id: z.number().int(), + data: personSchemaOptional, + }), + async handler({ ctx, input }) { + if (ctx.account.role === 'GLIEDERUNG_ADMIN') { + const { gliederungId: gliederungIdActor } = await prisma.person.findFirstOrThrow({ + where: { + account: { + id: ctx.accountId, + }, + }, + select: { + gliederungId: true, + }, + }) + const { gliederungId: gliederungIdTarget } = await prisma.person.findUniqueOrThrow({ + where: { + id: input.id, + }, + select: { + gliederungId: true, + }, + }) + + if (gliederungIdTarget !== gliederungIdActor) { + throw new TRPCError({ + code: 'NOT_FOUND', + }) + } + } + + await prisma.notfallkontakt.deleteMany({ + where: { + personId: input.id, + }, + }) + + return prisma.person.update({ + where: { + id: input.id, + }, + data: await getPersonCreateData(input.data), + select: { + id: true, + }, + }) + }, +}) diff --git a/apps/api/src/services/person/personVerwaltungPatch.ts b/apps/api/src/services/person/personVerwaltungPatch.ts deleted file mode 100644 index ecf3fa2a..00000000 --- a/apps/api/src/services/person/personVerwaltungPatch.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { Role } from '@prisma/client' -import z from 'zod' - -import prisma from '../../prisma.js' -import { defineProtectedMutateProcedure } from '../../types/defineProcedure.js' - -import { getPersonCreateData, personSchemaOptional } from './schema/person.schema.js' - -export const personVerwaltungPatchProcedure = defineProtectedMutateProcedure({ - key: 'verwaltungPatch', - roleIds: [Role.ADMIN], - inputSchema: z.strictObject({ - id: z.number().int(), - data: personSchemaOptional, - }), - async handler({ input }) { - await prisma.notfallkontakt.deleteMany({ - where: { - personId: input.id, - }, - }) - - return prisma.person.update({ - where: { - id: input.id, - }, - data: await getPersonCreateData(input.data), - select: { - id: true, - }, - }) - }, -}) diff --git a/apps/frontend/package.json b/apps/frontend/package.json index 4ec38e0b..9fda9fa3 100644 --- a/apps/frontend/package.json +++ b/apps/frontend/package.json @@ -37,6 +37,7 @@ "@vueuse/router": "^12.3.0", "clsx": "^2.1.1", "http2-proxy": "^5.0.53", + "human-filetypes": "^1.1.3", "intl-tel-input": "^24.4.0", "primevue": "^4.2.5", "radix-vue": "^1.9.5", diff --git a/apps/frontend/src/components/BasicInputs/InputFileUploadArea.vue b/apps/frontend/src/components/BasicInputs/InputFileUploadArea.vue index 2d2f78e0..d32a7f0e 100644 --- a/apps/frontend/src/components/BasicInputs/InputFileUploadArea.vue +++ b/apps/frontend/src/components/BasicInputs/InputFileUploadArea.vue @@ -2,6 +2,9 @@ import { computed, onMounted, ref, shallowRef } from 'vue' import { formatBytes } from '@codeanker/helpers' +import { TrashIcon } from '@heroicons/vue/24/solid' +import { fromMime, type FileKind } from 'human-filetypes' +import Button from '../UIComponents/Button.vue' const props = defineProps<{ multiple?: Multiple @@ -10,10 +13,7 @@ const props = defineProps<{ maxFileSize?: number fullWindowDropzone?: boolean uploadText?: string -}>() - -const emit = defineEmits<{ - uploaded: [Multiple extends true ? File[] : File] + onUpload: (files: Multiple extends true ? File[] : File) => Promise }>() const InputFileUploadAreaConf = { @@ -61,22 +61,30 @@ function dropZoneDrop(event) { event.preventDefault() event.stopPropagation() dragCounter.value = 0 - return upload(event.dataTransfer.files) + + files.value.push(...Array.from(event.dataTransfer.files)) } +const files = ref([]) + function uploadFileInput(event) { - return upload(event.target.files) + files.value.push(...Array.from(event.target.files)) } -async function upload(files) { - if (uploadPending.value || !files || !files[0]) { +function fileUrl(file: File) { + return URL.createObjectURL(file) +} + +async function upload() { + if (uploadPending.value || !files.value || !files.value[0]) { return } try { uploadPending.value = true error.value = null - const result = await handleUpload(files, props.multiple, props.maxFileSize) - emit('uploaded', result) + const result = await prepareUpload(files.value, props.multiple, props.maxFileSize) + await props.onUpload(result) + files.value = [] } catch (uploadError: any) { if (uploadError.message === 'file size too large' && props.maxFileSize) { error.value = `Die Dateigröße muss kleiner als ${formatBytes(props.maxFileSize)} sein` @@ -92,7 +100,7 @@ async function upload(files) { } } -async function handleUpload(uploadFiles, multiple = false, maxFileSize) { +async function prepareUpload(uploadFiles, multiple = false, maxFileSize) { if (multiple) { const toUploadFiles: any[] = [] for (const key in uploadFiles) { @@ -119,6 +127,22 @@ async function handleUpload(uploadFiles, multiple = false, maxFileSize) { } } } + +const mimeMap: Record = { + image: 'Bild', + video: 'Video', + audio: 'Audio', + archive: 'Archiv', + document: 'Dokument', + spreadsheet: 'Tabelle', + presentation: 'Präsentation', + font: 'Schriftart', + text: 'Text', + application: 'Ausführbare Datei', + unknown: 'Unbekanntes Dateiformat', +} + +defineExpose({ files }) diff --git a/apps/frontend/src/components/CustomFields/CustomFieldsFormEdit.vue b/apps/frontend/src/components/CustomFields/CustomFieldsFormEdit.vue index 1f356a0d..f57a3658 100644 --- a/apps/frontend/src/components/CustomFields/CustomFieldsFormEdit.vue +++ b/apps/frontend/src/components/CustomFields/CustomFieldsFormEdit.vue @@ -50,7 +50,7 @@ const { const data = form.value try { - await apiClient.customFields.veranstaltungUpdate.mutate({ + await apiClient.customFields.update.mutate({ fieldId: props.fieldId, data: { name: data.name, @@ -79,7 +79,7 @@ const { isLoading: isDeleting, } = useAsyncState( async () => { - await apiClient.customFields.veranstaltungDelete.mutate({ + await apiClient.customFields.delete.mutate({ fieldId: field.value!.id, veranstaltungId: 0, }) diff --git a/apps/frontend/src/components/CustomFields/CustomFieldsTable.vue b/apps/frontend/src/components/CustomFields/CustomFieldsTable.vue index 85c761af..674aa71e 100644 --- a/apps/frontend/src/components/CustomFields/CustomFieldsTable.vue +++ b/apps/frontend/src/components/CustomFields/CustomFieldsTable.vue @@ -34,7 +34,15 @@ const router = useRouter() type Field = (typeof fields.value)[number] function canEdit(field: Field) { - return loggedInAccount.value?.role === 'ADMIN' || field.unterveranstaltungId !== null + if (field.veranstaltungId !== null && loggedInAccount.value?.role === 'ADMIN') { + return true + } + + if (field.unterveranstaltungId !== null && loggedInAccount.value?.role !== 'USER') { + return true + } + + return false } function onClickRow(field: Field) { diff --git a/apps/frontend/src/components/LayoutComponents/PublicFooter.vue b/apps/frontend/src/components/LayoutComponents/PublicFooter.vue index 838e8505..430ea03b 100644 --- a/apps/frontend/src/components/LayoutComponents/PublicFooter.vue +++ b/apps/frontend/src/components/LayoutComponents/PublicFooter.vue @@ -5,7 +5,6 @@ const commitHash = import.meta.env.VITE_APP_COMMIT_HASH || '000000' import { injectUnterveranstaltung } from '@/layouts/AnmeldungLayout.vue' import { formatDate } from '@vueuse/core' const unterveranstaltung = injectUnterveranstaltung() -console.log(unterveranstaltung)