From 4b2fc5a455aeb341365f0b9b512b11c21456d3e7 Mon Sep 17 00:00:00 2001 From: Axel Rindle Date: Sat, 12 Apr 2025 14:23:35 +0200 Subject: [PATCH 01/76] wip vue datatable --- .../src/services/activity/activity.routes.ts | 5 +- .../api/src/services/activity/activityList.ts | 88 ++++--- apps/frontend/package.json | 4 + apps/frontend/src/App.vue | 6 + apps/frontend/src/components/LoadingBar.vue | 13 + .../src/components/Table/DataTable.vue | 225 ++++++++++++++++++ apps/frontend/src/components/Table/Filter.vue | 36 +++ apps/frontend/src/main.ts | 7 +- apps/frontend/src/types.d.ts | 24 ++ .../Verwaltung/Activity/ActivityList.vue | 155 ++++++------ apps/frontend/tailwind.config.js | 13 + 11 files changed, 449 insertions(+), 127 deletions(-) create mode 100644 apps/frontend/src/components/LoadingBar.vue create mode 100644 apps/frontend/src/components/Table/DataTable.vue create mode 100644 apps/frontend/src/components/Table/Filter.vue diff --git a/apps/api/src/services/activity/activity.routes.ts b/apps/api/src/services/activity/activity.routes.ts index 41990d1a..cef4239e 100644 --- a/apps/api/src/services/activity/activity.routes.ts +++ b/apps/api/src/services/activity/activity.routes.ts @@ -1,11 +1,10 @@ // Prettier ignored is because this file is generated import { mergeRouters } from '../../trpc.js' -import { activityListProcedure, activityCountProcedure } from './activityList.js' +import { activityListProcedure } from './activityList.js' // Import Routes here - do not delete this line export const activityRouter = mergeRouters( - activityListProcedure, - activityCountProcedure + activityListProcedure // Add Routes here - do not delete this line ) diff --git a/apps/api/src/services/activity/activityList.ts b/apps/api/src/services/activity/activityList.ts index d2070dcd..78177286 100644 --- a/apps/api/src/services/activity/activityList.ts +++ b/apps/api/src/services/activity/activityList.ts @@ -1,33 +1,44 @@ -import { type Prisma, Role } from '@prisma/client' +import { ActivityType, Prisma, Role } from '@prisma/client' import z from 'zod' import prisma from '../../prisma.js' import { defineProtectedQueryProcedure } from '../../types/defineProcedure.js' -import { defineQuery, getOrderBy } from '../../types/defineQuery.js' - -const inputSchema = defineQuery({ - filter: z.strictObject({ - veranstaltungId: z.string().optional(), - }), - orderBy: z.array( - z.tuple([z.union([z.literal('id'), z.literal('createdAt')]), z.union([z.literal('asc'), z.literal('desc')])]) - ), -}) - -type Input = z.infer export const activityListProcedure = defineProtectedQueryProcedure({ key: 'list', roleIds: [Role.ADMIN], - inputSchema, - async handler({ input, ctx }) { - const { skip, take } = input.pagination + inputSchema: z.strictObject({ + pagination: z + .strictObject({ + pageIndex: z.number().min(0), + pageSize: z.number().min(1).max(50), + }) + .optional(), + filter: z + .strictObject({ + type: z.nativeEnum(ActivityType), + }) + .partial() + .optional(), + }), + handler: async ({ input: { pagination, filter } }) => { + const where: Prisma.ActivityWhereInput = { + type: filter?.type, + } + + const total = await prisma.activity.count({ where }) + + const pageIndex = pagination?.pageIndex ?? 0 + const pageSize = pagination?.pageSize ?? 50 + const pages = Math.ceil(total / pageSize) const activities = await prisma.activity.findMany({ - skip, - take, - orderBy: getOrderBy(input.orderBy), - where: await getWhere(input.filter, ctx.account), + take: pageSize, + skip: pageSize * pageIndex, + orderBy: { + createdAt: 'desc', + }, + where, include: { causer: { select: { @@ -42,32 +53,15 @@ export const activityListProcedure = defineProtectedQueryProcedure({ }, }) - return activities - }, -}) - -export const activityCountProcedure = defineProtectedQueryProcedure({ - key: 'count', - roleIds: [Role.ADMIN], - inputSchema: inputSchema.pick({ filter: true }), - async handler({ input, ctx }) { - const activities = await prisma.activity.count({ - where: await getWhere(input.filter, ctx.account), - }) - - return activities + return { + data: activities, + total, + pagination: { + page: pageIndex, + pages, + hasNextPage: pageIndex < pages - 1, + hasPreviousPage: pageIndex > 0, + }, + } }, }) - -// eslint-disable-next-line @typescript-eslint/require-await -async function getWhere( - // eslint-disable-next-line @typescript-eslint/no-unused-vars - filter: Input['filter'], - // eslint-disable-next-line @typescript-eslint/no-unused-vars - account: { - id: number - role: Role - } -): Promise { - return {} -} diff --git a/apps/frontend/package.json b/apps/frontend/package.json index 9fda9fa3..ed99c938 100644 --- a/apps/frontend/package.json +++ b/apps/frontend/package.json @@ -23,6 +23,10 @@ "@heroicons/vue": "^2.0.18", "@tailwindcss/forms": "^0.5.7", "@tailwindcss/typography": "^0.5.16", + "@tanstack/vue-query": "^5.72.0", + "@tanstack/vue-query-devtools": "^5.72.2", + "@tanstack/vue-table": "^8.21.2", + "@tanstack/vue-virtual": "^3.13.6", "@tiptap/extension-document": "catalog:", "@tiptap/extension-highlight": "catalog:", "@tiptap/extension-link": "catalog:", diff --git a/apps/frontend/src/App.vue b/apps/frontend/src/App.vue index 98240aef..0024622f 100644 --- a/apps/frontend/src/App.vue +++ b/apps/frontend/src/App.vue @@ -1,3 +1,9 @@ + + diff --git a/apps/frontend/src/components/LoadingBar.vue b/apps/frontend/src/components/LoadingBar.vue new file mode 100644 index 00000000..32ffdddd --- /dev/null +++ b/apps/frontend/src/components/LoadingBar.vue @@ -0,0 +1,13 @@ + + + diff --git a/apps/frontend/src/components/Table/DataTable.vue b/apps/frontend/src/components/Table/DataTable.vue new file mode 100644 index 00000000..daf6c0d1 --- /dev/null +++ b/apps/frontend/src/components/Table/DataTable.vue @@ -0,0 +1,225 @@ + + + diff --git a/apps/frontend/src/components/Table/Filter.vue b/apps/frontend/src/components/Table/Filter.vue new file mode 100644 index 00000000..b29ef67b --- /dev/null +++ b/apps/frontend/src/components/Table/Filter.vue @@ -0,0 +1,36 @@ + + + diff --git a/apps/frontend/src/main.ts b/apps/frontend/src/main.ts index 12012725..8ceaca61 100644 --- a/apps/frontend/src/main.ts +++ b/apps/frontend/src/main.ts @@ -1,11 +1,12 @@ -import { createApp } from 'vue' +import './assets/main.scss' +import { createApp } from 'vue' +import { VueQueryPlugin } from '@tanstack/vue-query' import App from './App.vue' import router from './router' -import './assets/main.scss' import PrimeVue from 'primevue/config' -createApp(App).use(router).use(PrimeVue).mount('#app') +createApp(App).use(router).use(PrimeVue).use(VueQueryPlugin).mount('#app') // Set Document Title relate to the Hostname document.title = window.location.hostname diff --git a/apps/frontend/src/types.d.ts b/apps/frontend/src/types.d.ts index d398e480..d9fedf42 100644 --- a/apps/frontend/src/types.d.ts +++ b/apps/frontend/src/types.d.ts @@ -1,4 +1,6 @@ +import '@tanstack/vue-table' import type { RouteLocationNormalizedLoadedGeneric, RouteLocationRaw } from 'vue-router' +import type { Option } from './components/BasicInputs/BasicSelect.vue' type Breadcrumb = { text: string @@ -13,3 +15,25 @@ declare module 'vue-router' { breadcrumbs?: BreadcrumbDefinition[] } } + +declare module '@tanstack/vue-table' { + interface ColumnMeta { + filter?: + | { + type: 'select' | 'multi-select' + options: Option[] + } + | { + type: 'text' + } + | { + type: 'date' + } + | { + type: 'date-range' + } + | { + type: 'number' + } + } +} diff --git a/apps/frontend/src/views/Verwaltung/Activity/ActivityList.vue b/apps/frontend/src/views/Verwaltung/Activity/ActivityList.vue index 7cc7902d..2134c236 100644 --- a/apps/frontend/src/views/Verwaltung/Activity/ActivityList.vue +++ b/apps/frontend/src/views/Verwaltung/Activity/ActivityList.vue @@ -1,12 +1,15 @@ diff --git a/apps/frontend/tailwind.config.js b/apps/frontend/tailwind.config.js index abde06fd..91e30c18 100644 --- a/apps/frontend/tailwind.config.js +++ b/apps/frontend/tailwind.config.js @@ -66,6 +66,19 @@ module.exports = { boxShadow: { hover: '0px 0px 12px 0px rgba(0,0,0,0.06);', }, + keyframes: { + progress: { + '0%': { transform: ' translateX(0) scaleX(0)' }, + '40%': { transform: 'translateX(0) scaleX(0.4)' }, + '100%': { transform: 'translateX(100%) scaleX(0.5)' }, + }, + }, + animation: { + progress: 'progress 1s infinite linear', + }, + transformOrigin: { + 'left-right': '0% 50%', + }, }, }, safelist: [ From 83e01594d7825a582c0db5c27bad1f882db5a67c Mon Sep 17 00:00:00 2001 From: Axel Rindle Date: Mon, 14 Apr 2025 21:29:25 +0200 Subject: [PATCH 02/76] feat: allow async option loading --- .../src/services/activity/activity.routes.ts | 5 +- .../api/src/services/activity/activityList.ts | 28 ++++++ .../src/components/Table/DataTable.vue | 30 ++++-- apps/frontend/src/components/Table/Filter.vue | 58 +++++++---- apps/frontend/src/types.d.ts | 2 +- .../Verwaltung/Activity/ActivityList.vue | 18 +++- pnpm-lock.yaml | 99 +++++++++++++++++-- 7 files changed, 201 insertions(+), 39 deletions(-) diff --git a/apps/api/src/services/activity/activity.routes.ts b/apps/api/src/services/activity/activity.routes.ts index cef4239e..1200b600 100644 --- a/apps/api/src/services/activity/activity.routes.ts +++ b/apps/api/src/services/activity/activity.routes.ts @@ -1,10 +1,11 @@ // Prettier ignored is because this file is generated import { mergeRouters } from '../../trpc.js' -import { activityListProcedure } from './activityList.js' +import { activityCompleteSubjectsProcedure, activityListProcedure } from './activityList.js' // Import Routes here - do not delete this line export const activityRouter = mergeRouters( - activityListProcedure + activityListProcedure, + activityCompleteSubjectsProcedure // Add Routes here - do not delete this line ) diff --git a/apps/api/src/services/activity/activityList.ts b/apps/api/src/services/activity/activityList.ts index 78177286..e5721bf8 100644 --- a/apps/api/src/services/activity/activityList.ts +++ b/apps/api/src/services/activity/activityList.ts @@ -3,6 +3,7 @@ import z from 'zod' import prisma from '../../prisma.js' import { defineProtectedQueryProcedure } from '../../types/defineProcedure.js' +import dayjs from 'dayjs' export const activityListProcedure = defineProtectedQueryProcedure({ key: 'list', @@ -16,14 +17,25 @@ export const activityListProcedure = defineProtectedQueryProcedure({ .optional(), filter: z .strictObject({ + createdAt: z + .tuple([z.date(), z.date()]) + .transform(([from, to]) => [dayjs(from).startOf('day').toDate(), dayjs(to).endOf('day').toDate()]), type: z.nativeEnum(ActivityType), + subjectType: z.string(), }) .partial() .optional(), }), handler: async ({ input: { pagination, filter } }) => { const where: Prisma.ActivityWhereInput = { + createdAt: filter?.createdAt + ? { + gte: filter.createdAt[0], + lte: filter.createdAt[1], + } + : undefined, type: filter?.type, + subjectType: filter?.subjectType, } const total = await prisma.activity.count({ where }) @@ -65,3 +77,19 @@ export const activityListProcedure = defineProtectedQueryProcedure({ } }, }) + +export const activityCompleteSubjectsProcedure = defineProtectedQueryProcedure({ + key: 'listSubjectTypes', + inputSchema: z.void(), + roleIds: [Role.ADMIN], + handler: async () => { + const result = await prisma.activity.findMany({ + distinct: ['subjectType'], + select: { + subjectType: true, + }, + }) + + return result.map((r) => r.subjectType) + }, +}) diff --git a/apps/frontend/src/components/Table/DataTable.vue b/apps/frontend/src/components/Table/DataTable.vue index daf6c0d1..5ef19644 100644 --- a/apps/frontend/src/components/Table/DataTable.vue +++ b/apps/frontend/src/components/Table/DataTable.vue @@ -1,10 +1,12 @@ @@ -215,16 +230,21 @@ const filterContainer = useTemplateRef('filterContainer') @click="emit('click', row.original)" @dblclick="emit('dblclick', row.original)" > - - - + + + + @@ -246,17 +266,22 @@ const filterContainer = useTemplateRef('filterContainer') v-for="footerGroup in table.getFooterGroups()" :key="footerGroup.id" > - - - + + + + diff --git a/apps/frontend/src/components/Table/initialData.ts b/apps/frontend/src/components/Table/initialData.ts new file mode 100644 index 00000000..ec668995 --- /dev/null +++ b/apps/frontend/src/components/Table/initialData.ts @@ -0,0 +1,10 @@ +export default { + data: [], + total: 0, + pagination: { + page: 0, + pages: 0, + hasNextPage: false, + hasPreviousPage: false, + }, +} diff --git a/apps/frontend/src/components/data/AnmeldungenTable.vue b/apps/frontend/src/components/data/AnmeldungenTable.vue index 653399b6..5164f42d 100644 --- a/apps/frontend/src/components/data/AnmeldungenTable.vue +++ b/apps/frontend/src/components/data/AnmeldungenTable.vue @@ -1,16 +1,15 @@ From a97d8c975b6c3de1ed0d4e558f272a861fed3732 Mon Sep 17 00:00:00 2001 From: Axel Rindle Date: Sat, 3 May 2025 13:31:47 +0200 Subject: [PATCH 10/76] update person table --- apps/api/src/services/person/person.router.ts | 5 +- apps/api/src/services/person/personGet.ts | 7 +- apps/api/src/services/person/personList.ts | 123 ++++++------ .../src/components/data/PersonTable.vue | 178 +++++++++--------- 4 files changed, 155 insertions(+), 158 deletions(-) diff --git a/apps/api/src/services/person/person.router.ts b/apps/api/src/services/person/person.router.ts index 97344110..b9050313 100644 --- a/apps/api/src/services/person/person.router.ts +++ b/apps/api/src/services/person/person.router.ts @@ -2,9 +2,9 @@ import { mergeRouters } from '../../trpc.js' import { personAuthenticatedGetProcedure } from './personAuthenticatedGet.js' import { personGetProcedure } from './personGet.js' -import { personCountProcedure, personListProcedure } from './personList.js' -import { personVerwaltungCreateProcedure } from './personVerwaltungCreate.js' +import { personListProcedure } from './personList.js' import { personVerwaltungPatchProcedure } from './personPatch.js' +import { personVerwaltungCreateProcedure } from './personVerwaltungCreate.js' import { personVerwaltungRemoveProcedure } from './personVerwaltungRemove.js' // Import Routes here - do not delete this line @@ -12,7 +12,6 @@ export const personRouter = mergeRouters( personAuthenticatedGetProcedure, personVerwaltungCreateProcedure, personListProcedure, - personCountProcedure, personGetProcedure, personVerwaltungPatchProcedure, personVerwaltungRemoveProcedure diff --git a/apps/api/src/services/person/personGet.ts b/apps/api/src/services/person/personGet.ts index a4f77bb0..4eec35d1 100644 --- a/apps/api/src/services/person/personGet.ts +++ b/apps/api/src/services/person/personGet.ts @@ -11,13 +11,14 @@ export const personGetProcedure = defineProtectedQueryProcedure({ inputSchema: z.strictObject({ id: z.number().int(), }), - handler: ({ ctx, input }) => { + handler: async ({ ctx, input }) => { + const protection = await getPersonProtectionFilter(ctx) const where: Prisma.PersonWhereUniqueInput = { - ...getPersonProtectionFilter(ctx), + ...protection, id: input.id, } - return prisma.person.findUniqueOrThrow({ + return await prisma.person.findUniqueOrThrow({ where, select: { id: true, diff --git a/apps/api/src/services/person/personList.ts b/apps/api/src/services/person/personList.ts index c577ef38..a5784bb6 100644 --- a/apps/api/src/services/person/personList.ts +++ b/apps/api/src/services/person/personList.ts @@ -2,37 +2,57 @@ import { Prisma, Role } from '@prisma/client' import z from 'zod' import prisma from '../../prisma.js' -import { defineProtectedQueryProcedure } from '../../types/defineProcedure.js' -import { defineQuery, getOrderBy } from '../../types/defineQuery.js' import type { AuthenticatedContext } from '../../trpc.js' - -const inputSchema = defineQuery({ - filter: z.strictObject({ - name: z.string().optional(), - gliederungName: z.string().optional(), - }), - orderBy: z.array( - z.tuple([ - z.union([z.literal('firstname'), z.literal('birthday'), z.literal('gliederung.name')]), - z.union([z.literal('asc'), z.literal('desc')]), - ]) - ), -}) - -type TInput = z.infer +import { defineProtectedQueryProcedure } from '../../types/defineProcedure.js' +import { calculatePagination, defineQueryResponse, defineTableInput } from '../../types/defineTableProcedure.js' +import { getGliederungRequireAdmin } from '../../util/getGliederungRequireAdmin.js' export const personListProcedure = defineProtectedQueryProcedure({ key: 'list', roleIds: [Role.ADMIN, Role.USER], - inputSchema, - handler: ({ ctx, input }) => { - const { skip, take } = input.pagination + inputSchema: defineTableInput({ + filter: { + name: z.string(), + gliederung_name: z.string(), + // gliederungId: z.number().int(), + // veranstaltungId: z.number().int(), + }, + }), + handler: async ({ ctx, input: { filter, pagination } }) => { + const protection = await getPersonProtectionFilter(ctx) + const where: Prisma.PersonWhereInput = { + OR: filter?.name + ? [ + { + firstname: { + contains: filter.name, + mode: 'insensitive', + }, + }, + { + lastname: { + contains: filter.name, + mode: 'insensitive', + }, + }, + ] + : undefined, + gliederung: { + name: filter?.gliederung_name, + }, + ...protection, + } - return prisma.person.findMany({ - skip, - take, - where: getWhere(input.filter, ctx), - orderBy: getOrderBy(input.orderBy), + const total = await prisma.person.count({ where }) + const { pageIndex, pageSize, pages } = calculatePagination(total, pagination) + + const persons = await prisma.person.findMany({ + take: pageSize, + skip: pageSize * pageIndex, + orderBy: { + lastname: 'asc', + }, + where, select: { id: true, firstname: true, @@ -41,20 +61,20 @@ export const personListProcedure = defineProtectedQueryProcedure({ photoId: true, gliederung: { select: { - id: true, name: true, }, }, account: { select: { id: true, - activatedAt: true, - role: true, - status: true, - GliederungToAccount: { + }, + }, + anmeldungen: { + select: { + _count: true, + unterveranstaltung: { select: { - role: true, - gliederung: { + veranstaltung: { select: { name: true, }, @@ -65,24 +85,14 @@ export const personListProcedure = defineProtectedQueryProcedure({ }, }, }) - }, -}) -export const personCountProcedure = defineProtectedQueryProcedure({ - key: 'count', - roleIds: [Role.ADMIN, Role.USER], - inputSchema: inputSchema.pick({ filter: true }), - async handler(options) { - const total = await prisma.person.count({ - where: getWhere(options.input.filter, options.ctx), - }) - return total + return defineQueryResponse({ data: persons, total, pagination: { pageIndex, pageSize, pages } }) }, }) -export function getPersonProtectionFilter({ +export async function getPersonProtectionFilter({ account, -}: AuthenticatedContext): Prisma.PersonWhereInput | Prisma.PersonWhereUniqueInput { +}: AuthenticatedContext): Promise { const where: Prisma.PersonWhereInput = {} if (account.role === Role.USER) { @@ -91,27 +101,10 @@ export function getPersonProtectionFilter({ accountId: account.id, }, } + } else if (account.role === Role.GLIEDERUNG_ADMIN) { + const gliederung = await getGliederungRequireAdmin(account.id) + where.gliederungId = gliederung.id } return where } - -function getWhere(filter: TInput['filter'], ctx: AuthenticatedContext): Prisma.PersonWhereInput { - const where: Prisma.PersonWhereInput = {} - - if (filter.name != null && filter.name != '') { - where.OR = [{ firstname: { contains: filter.name } }, { lastname: { contains: filter.name } }] - } - if (filter.gliederungName != null && filter.gliederungName != '') { - where.gliederung = { - name: { - contains: filter.gliederungName, - }, - } - } - - return { - ...where, - ...getPersonProtectionFilter(ctx), - } -} diff --git a/apps/frontend/src/components/data/PersonTable.vue b/apps/frontend/src/components/data/PersonTable.vue index 6794f671..e151b8c7 100644 --- a/apps/frontend/src/components/data/PersonTable.vue +++ b/apps/frontend/src/components/data/PersonTable.vue @@ -1,108 +1,112 @@ From a8abd4c113d60351703e3e69a205f2a3de046dd4 Mon Sep 17 00:00:00 2001 From: Axel Rindle Date: Sat, 3 May 2025 13:31:59 +0200 Subject: [PATCH 11/76] fix dashboard --- apps/frontend/src/views/Dashboard/Gliederung.vue | 7 ++++--- apps/frontend/src/views/Dashboard/Verwaltung.vue | 7 ++++--- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/apps/frontend/src/views/Dashboard/Gliederung.vue b/apps/frontend/src/views/Dashboard/Gliederung.vue index c1767e64..1222d08a 100644 --- a/apps/frontend/src/views/Dashboard/Gliederung.vue +++ b/apps/frontend/src/views/Dashboard/Gliederung.vue @@ -3,15 +3,16 @@ import { useAsyncState } from '@vueuse/core' import { apiClient } from '@/api' import VeranstaltungCard from '@/components/UIComponents/VeranstaltungCard.vue' +import initialData from '@/components/Table/initialData' const { state: veranstaltungen } = useAsyncState(async () => { - return apiClient.veranstaltung.gliederungList.query({ filter: {}, orderBy: [], pagination: { take: 100, skip: 0 } }) -}, []) + return apiClient.veranstaltung.verwaltungList.query({}) +}, initialData)