Skip to content
Open
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,5 @@
-- AlterTable
ALTER TABLE "faq_categories" ADD COLUMN "order" INTEGER NOT NULL DEFAULT 0;

-- AlterTable
ALTER TABLE "faqs" ADD COLUMN "order" INTEGER NOT NULL DEFAULT 0;
6 changes: 4 additions & 2 deletions apps/api/prisma/schema/Faq.prisma
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
model FaqCategory {
id String @id @default(uuid(7))
name String
id String @id @default(uuid(7))
name String
order Int @default(0)

unterveranstaltungId String
unterveranstaltung Unterveranstaltung @relation(fields: [unterveranstaltungId], references: [id])
Expand All @@ -14,6 +15,7 @@ model Faq {
id String @id @default(uuid(7))
question String
answer String
order Int @default(0)

categoryId String
category FaqCategory @relation(fields: [categoryId], references: [id])
Expand Down
13 changes: 13 additions & 0 deletions apps/api/src/services/faqs/faqCreateProcedure.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,22 @@ export const faqCreateProcedure = defineProtectedMutateProcedure({
faq: faqSchema,
}),
handler: async ({ input: { faq, unterveranstaltungId } }) => {
const [maxCategoryOrder, maxFaqOrder] = await Promise.all([
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ich habe ein Reordering schon für Custom Fields gebaut, da kannst du dir das meiste abschauen. Sollte einheitlich sein.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, ich seh, dass dort eine weitere Tabelle hinter "Reihenfolge ändern" steckt, wo man die via buttons nach oben und unten bewegen kann.
Ich denke das ist hier eher weniger sinnvoll, da die FAQs und Kategorien nicht in einer Tabellarisch Form dargestellt werden.

Man könnte überlegen das Drag-n-Drop feature via ein "Reihenfolge ändern" button/toggle ein/ausschalten.

prisma.faqCategory.aggregate({
where: { unterveranstaltungId },
_max: { order: true },
}),
prisma.faq.aggregate({
where: { category: { unterveranstaltungId } },
_max: { order: true },
}),
])

await prisma.faq.create({
data: {
question: faq.question,
answer: faq.answer,
order: (maxFaqOrder._max.order ?? -1) + 1,
unterveranstaltung: {
connect: {
id: unterveranstaltungId,
Expand All @@ -26,6 +38,7 @@ export const faqCreateProcedure = defineProtectedMutateProcedure({
create: {
name: faq.category,
unterveranstaltungId,
order: (maxCategoryOrder._max.order ?? -1) + 1,
},
where: {
name_unterveranstaltungId: {
Expand Down
29 changes: 28 additions & 1 deletion apps/api/src/services/faqs/faqDeleteProcecure.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,39 @@ import z from 'zod'

import prisma from '../../prisma.js'
import { defineProtectedMutateProcedure } from '../../types/defineProcedure.js'
import { getGliederungRequireAdmin } from '../../util/getGliederungRequireAdmin.js'
import { TRPCError } from '@trpc/server'

export const faqDeleteProcedure = defineProtectedMutateProcedure({
key: 'delete',
roleIds: ['ADMIN', 'GLIEDERUNG_ADMIN'],
inputSchema: z.string().uuid(),
handler: async ({ input }) => {
handler: async ({ ctx, input }) => {
const gliederung = await getGliederungRequireAdmin(ctx.accountId)
const unterveranstaltungIds = (
await prisma.unterveranstaltung.findMany({
where: { gliederungId: gliederung.id },
select: { id: true },
})
).map((u) => u.id)

const faq = await prisma.faq.findUnique({
where: {
id: input,
unterveranstaltung: {
every: {
id: { in: unterveranstaltungIds },
},
},
},
select: {
id: true,
},
})
if (!faq) {
throw new TRPCError({ code: 'FORBIDDEN', message: `FAQ do not belong to accounts gliederung` })
}

await prisma.faq.delete({
where: {
id: input,
Expand Down
17 changes: 15 additions & 2 deletions apps/api/src/services/faqs/faqListProcedure.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,24 +13,37 @@ export async function listFaqs(unterveranstaltungId: string) {
},
},
},
orderBy: {
order: 'asc',
},
select: {
id: true,
question: true,
answer: true,
order: true,
category: {
select: {
id: true,
name: true,
order: true,
},
},
},
})

const groups = groupBy(
list.map((v) => ({ ...v, category: v.category.name })),
list.map((v) => ({
...v,
category: v.category.name,
categoryId: v.category.id,
categoryOrder: v.category.order,
})),
({ 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]?.categoryOrder ?? 0) - (b[0]?.categoryOrder ?? 0))
)
}

export const faqListProcedure = defineProtectedQueryProcedure({
Expand Down
95 changes: 95 additions & 0 deletions apps/api/src/services/faqs/faqReorderProcedure.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import z from 'zod'

import prisma from '../../prisma.js'
import { TRPCError } from '@trpc/server'
import { Role } from '@prisma/client'
import { defineProtectedMutateProcedure } from '../../types/defineProcedure.js'
import { getGliederungRequireAdmin } from '../../util/getGliederungRequireAdmin.js'

export const faqReorderProcedure = defineProtectedMutateProcedure({
key: 'reorder',
roleIds: ['ADMIN', 'GLIEDERUNG_ADMIN'],
inputSchema: z.strictObject({
faqOrder: z.array(
z.strictObject({
id: z.string().uuid(),
order: z.number().int().min(0),
categoryId: z.string().uuid(),
})
),
categoryOrder: z.array(
z.strictObject({
id: z.string().uuid(),
order: z.number().int().min(0),
})
),
}),
handler: async ({ ctx, input: { faqOrder, categoryOrder } }) => {
/// Check permissions for FAQs and categories
if (ctx.account.role !== Role.ADMIN) {
const gliederung = await getGliederungRequireAdmin(ctx.accountId)
const unterveranstaltungIds = (
await prisma.unterveranstaltung.findMany({
where: { gliederungId: gliederung.id },
select: { id: true },
})
).map((u) => u.id)

const faqs = await prisma.faq.findMany({
where: { id: { in: faqOrder.map((f) => f.id) } },
select: {
id: true,
unterveranstaltung: {
select: {
id: true,
},
},
},
})
for (const faq of faqs) {
const unterveranstaltungId = faq.unterveranstaltung[0]?.id
if (!unterveranstaltungId || !unterveranstaltungIds.includes(unterveranstaltungId)) {
throw new TRPCError({ code: 'FORBIDDEN', message: `FAQ '${faq.id}' do not belong to accounts gliederung` })
}
}

const categoryIds = [...new Set([...categoryOrder.map((c) => c.id), ...faqOrder.map((f) => f.categoryId)])]
const categories = await prisma.faqCategory.findMany({
where: { id: { in: categoryIds } },
select: {
id: true,
unterveranstaltung: {
select: {
id: true,
},
},
},
})
for (const category of categories) {
const unterveranstaltungId = category.unterveranstaltung.id
if (!unterveranstaltungId || !unterveranstaltungIds.includes(unterveranstaltungId)) {
throw new TRPCError({
code: 'FORBIDDEN',
message: `FAQ Category '${category.id}' do not belong to accounts gliederung`,
})
}
}
}

/// Perform updates in a transaction
await prisma.$transaction([
...faqOrder.map(({ id, order, categoryId }) =>
prisma.faq.update({
where: { id },
data: { order, categoryId },
})
),
...categoryOrder.map(({ id, order }) =>
prisma.faqCategory.update({
where: { id },
data: { order },
})
),
])
},
})
4 changes: 3 additions & 1 deletion apps/api/src/services/faqs/faqs.router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -12,6 +13,7 @@ export const faqsRouter = mergeRouters(
faqCategorySearchProcedure,
faqCreateProcedure,
faqUpdateProcedure,
faqDeleteProcedure
faqDeleteProcedure,
faqReorderProcedure
// Add Routes here - do not delete this line
)
3 changes: 2 additions & 1 deletion apps/frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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:*",
Expand Down
18 changes: 17 additions & 1 deletion apps/frontend/src/views/FAQs/FAQFormModal.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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<ModalApi>({
show() {
modal.value?.show()
Expand Down Expand Up @@ -127,7 +134,16 @@ defineExpose<ModalApi>({
/>
</div>

<div class="flex justify-end col-span-2 mt-8">
<div class="flex justify-between col-span-2 mt-8">
<Button
v-if="isEdit"
type="button"
color="danger"
@click="onDelete"
>
Entfernen
</Button>
<span v-else />
<Button type="submit"> Speichern </Button>
</div>
</ValidateForm>
Expand Down
Loading
Loading