From ae4593a2229b72b39c162cb32bdc5f2d413da04c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Momme=20J=C3=BCrgensen?= Date: Wed, 11 Mar 2026 19:05:35 +0100 Subject: [PATCH 1/3] feat: faq sort via vuedraggable feat: faq delete fix: faq create after edit bug --- .../migration.sql | 5 + apps/api/prisma/schema/Faq.prisma | 12 +- .../src/services/faqs/faqCreateProcedure.ts | 13 +++ .../api/src/services/faqs/faqListProcedure.ts | 17 ++- .../src/services/faqs/faqReorderProcedure.ts | 40 +++++++ apps/api/src/services/faqs/faqs.router.ts | 4 +- apps/frontend/package.json | 3 +- apps/frontend/src/views/FAQs/FAQFormModal.vue | 18 ++- apps/frontend/src/views/FAQs/FAQList.vue | 106 +++++++++++++----- .../UnterveranstaltungDetail.vue | 26 ++--- pnpm-lock.yaml | 16 ++- 11 files changed, 209 insertions(+), 51 deletions(-) create mode 100644 apps/api/prisma/migrations/20260311172026_faq_sort_order/migration.sql create mode 100644 apps/api/src/services/faqs/faqReorderProcedure.ts diff --git a/apps/api/prisma/migrations/20260311172026_faq_sort_order/migration.sql b/apps/api/prisma/migrations/20260311172026_faq_sort_order/migration.sql new file mode 100644 index 00000000..b412f2a2 --- /dev/null +++ b/apps/api/prisma/migrations/20260311172026_faq_sort_order/migration.sql @@ -0,0 +1,5 @@ +-- AlterTable +ALTER TABLE "faq_categories" ADD COLUMN "sortOrder" INTEGER NOT NULL DEFAULT 0; + +-- AlterTable +ALTER TABLE "faqs" ADD COLUMN "sortOrder" INTEGER NOT NULL DEFAULT 0; diff --git a/apps/api/prisma/schema/Faq.prisma b/apps/api/prisma/schema/Faq.prisma index 1db11286..21facb64 100644 --- a/apps/api/prisma/schema/Faq.prisma +++ b/apps/api/prisma/schema/Faq.prisma @@ -1,6 +1,7 @@ model FaqCategory { - id String @id @default(uuid(7)) - name String + id String @id @default(uuid(7)) + name String + sortOrder Int @default(0) unterveranstaltungId String unterveranstaltung Unterveranstaltung @relation(fields: [unterveranstaltungId], references: [id]) @@ -11,9 +12,10 @@ model FaqCategory { } model Faq { - id String @id @default(uuid(7)) - question String - answer String + id String @id @default(uuid(7)) + question String + answer String + sortOrder Int @default(0) categoryId String category FaqCategory @relation(fields: [categoryId], references: [id]) diff --git a/apps/api/src/services/faqs/faqCreateProcedure.ts b/apps/api/src/services/faqs/faqCreateProcedure.ts index f9df6a43..d52ddaa7 100644 --- a/apps/api/src/services/faqs/faqCreateProcedure.ts +++ b/apps/api/src/services/faqs/faqCreateProcedure.ts @@ -12,10 +12,22 @@ export const faqCreateProcedure = defineProtectedMutateProcedure({ faq: faqSchema, }), handler: async ({ input: { faq, unterveranstaltungId } }) => { + const [maxCategoryOrder, maxFaqOrder] = await Promise.all([ + prisma.faqCategory.aggregate({ + where: { unterveranstaltungId }, + _max: { sortOrder: true }, + }), + prisma.faq.aggregate({ + where: { category: { unterveranstaltungId } }, + _max: { sortOrder: true }, + }), + ]) + await prisma.faq.create({ data: { question: faq.question, answer: faq.answer, + sortOrder: (maxFaqOrder._max.sortOrder ?? -1) + 1, unterveranstaltung: { connect: { id: unterveranstaltungId, @@ -26,6 +38,7 @@ export const faqCreateProcedure = defineProtectedMutateProcedure({ create: { name: faq.category, unterveranstaltungId, + sortOrder: (maxCategoryOrder._max.sortOrder ?? -1) + 1, }, where: { name_unterveranstaltungId: { diff --git a/apps/api/src/services/faqs/faqListProcedure.ts b/apps/api/src/services/faqs/faqListProcedure.ts index 14c9b5ff..eae1c904 100644 --- a/apps/api/src/services/faqs/faqListProcedure.ts +++ b/apps/api/src/services/faqs/faqListProcedure.ts @@ -13,24 +13,37 @@ export async function listFaqs(unterveranstaltungId: string) { }, }, }, + orderBy: { + sortOrder: 'asc', + }, select: { id: true, question: true, answer: true, + sortOrder: true, category: { select: { + id: true, name: true, + sortOrder: true, }, }, }, }) const groups = groupBy( - list.map((v) => ({ ...v, category: v.category.name })), + list.map((v) => ({ + ...v, + category: v.category.name, + categoryId: v.category.id, + categorySortOrder: v.category.sortOrder, + })), ({ category }) => category ) - return Object.fromEntries(Object.entries(groups).sort(([a], [b]) => a.localeCompare(b))) + return Object.fromEntries( + Object.entries(groups).sort(([, a], [, b]) => (a[0]?.categorySortOrder ?? 0) - (b[0]?.categorySortOrder ?? 0)) + ) } export const faqListProcedure = defineProtectedQueryProcedure({ diff --git a/apps/api/src/services/faqs/faqReorderProcedure.ts b/apps/api/src/services/faqs/faqReorderProcedure.ts new file mode 100644 index 00000000..c2f208cf --- /dev/null +++ b/apps/api/src/services/faqs/faqReorderProcedure.ts @@ -0,0 +1,40 @@ +import z from 'zod' + +import prisma from '../../prisma.js' +import { defineProtectedMutateProcedure } from '../../types/defineProcedure.js' + +export const faqReorderProcedure = defineProtectedMutateProcedure({ + key: 'reorder', + roleIds: ['ADMIN', 'GLIEDERUNG_ADMIN'], + inputSchema: z.strictObject({ + faqOrder: z.array( + z.strictObject({ + id: z.string().uuid(), + sortOrder: z.number().int().min(0), + categoryId: z.string().uuid(), + }) + ), + categoryOrder: z.array( + z.strictObject({ + id: z.string().uuid(), + sortOrder: z.number().int().min(0), + }) + ), + }), + handler: async ({ input: { faqOrder, categoryOrder } }) => { + await prisma.$transaction([ + ...faqOrder.map(({ id, sortOrder, categoryId }) => + prisma.faq.update({ + where: { id }, + data: { sortOrder, categoryId }, + }) + ), + ...categoryOrder.map(({ id, sortOrder }) => + prisma.faqCategory.update({ + where: { id }, + data: { sortOrder }, + }) + ), + ]) + }, +}) diff --git a/apps/api/src/services/faqs/faqs.router.ts b/apps/api/src/services/faqs/faqs.router.ts index 9d137081..5436996a 100644 --- a/apps/api/src/services/faqs/faqs.router.ts +++ b/apps/api/src/services/faqs/faqs.router.ts @@ -4,6 +4,7 @@ import { faqCreateProcedure } from './faqCreateProcedure.js' import { faqDeleteProcedure } from './faqDeleteProcecure.js' import { faqCategorySearchProcedure, faqListProcedure } from './faqListProcedure.js' import { faqUpdateProcedure } from './faqUpdateProcedure.js' +import { faqReorderProcedure } from './faqReorderProcedure.js' // Import Routes here - do not delete this line @@ -12,6 +13,7 @@ export const faqsRouter = mergeRouters( faqCategorySearchProcedure, faqCreateProcedure, faqUpdateProcedure, - faqDeleteProcedure + faqDeleteProcedure, + faqReorderProcedure // Add Routes here - do not delete this line ) diff --git a/apps/frontend/package.json b/apps/frontend/package.json index 773d5512..8eb5f28e 100644 --- a/apps/frontend/package.json +++ b/apps/frontend/package.json @@ -51,7 +51,8 @@ "vaul-vue": "^0.4.1", "vue": "catalog:", "vue-router": "^4.2.5", - "vue-sonner": "^1.3.0" + "vue-sonner": "^1.3.0", + "vuedraggable": "^4.1.0" }, "devDependencies": { "@codeanker/eslint-config": "workspace:*", diff --git a/apps/frontend/src/views/FAQs/FAQFormModal.vue b/apps/frontend/src/views/FAQs/FAQFormModal.vue index b38162bc..b926ec93 100644 --- a/apps/frontend/src/views/FAQs/FAQFormModal.vue +++ b/apps/frontend/src/views/FAQs/FAQFormModal.vue @@ -79,6 +79,13 @@ async function onSubmit() { emit('success') } +async function onDelete() { + if (!props.faq) return + await apiClient.faq.delete.mutate(props.faq.id) + modal.value?.hide() + emit('success') +} + defineExpose({ show() { modal.value?.show() @@ -127,7 +134,16 @@ defineExpose({ /> -
+
+ +
diff --git a/apps/frontend/src/views/FAQs/FAQList.vue b/apps/frontend/src/views/FAQs/FAQList.vue index d35e067c..9c3d0f80 100644 --- a/apps/frontend/src/views/FAQs/FAQList.vue +++ b/apps/frontend/src/views/FAQs/FAQList.vue @@ -1,9 +1,12 @@