From 900414727fb609f8035d43738ebdfdcbb75fec1b Mon Sep 17 00:00:00 2001 From: Pascal Syma Date: Wed, 13 Aug 2025 23:50:57 +0200 Subject: [PATCH] Move review to separate model --- TODO.md | 6 +- app/components/QuestTags.vue | 6 +- app/components/admin/Answer.vue | 37 +++-- app/components/admin/Team.vue | 2 +- .../hunt/[huntSlug]/[questSlug]/index.vue | 102 +++++++++---- app/pages/hunt/[huntSlug]/admin.vue | 2 +- app/pages/hunt/[huntSlug]/index.vue | 11 +- .../migration.sql | 62 ++++++++ prisma/schema.prisma | 27 +++- prisma/seed/hunt.ts | 135 +++++++++++------- .../admin/answer/[answerId]/review.post.ts | 21 ++- .../api/admin/hunt/[huntId]/showcase.get.ts | 42 ++++-- .../api/admin/hunt/[huntId]/teamless.get.ts | 2 +- server/api/admin/quest/[questId]/index.get.ts | 26 ++-- server/api/hunt/[huntId]/index.get.ts | 35 ++++- server/api/quest/[questId]/index.get.ts | 20 ++- server/api/quest/[questId]/submit.post.ts | 22 ++- 17 files changed, 403 insertions(+), 155 deletions(-) create mode 100644 prisma/migrations/20250813195841_move_review_to_seperate_model/migration.sql diff --git a/TODO.md b/TODO.md index 2b93361..27d23e2 100644 --- a/TODO.md +++ b/TODO.md @@ -7,7 +7,7 @@ - [x] Start/Stop - [ ] Add Reviews/Score - [x] View best Answers and Pictures - - [ ] All/Best Images gallery + - [x] All/Best Images gallery - [x] Manage teams (remove, kick, add) - [x] Manage admins (kick, add) - **Hunt User** @@ -18,7 +18,7 @@ - [x] Change/Update Answer and image - [x] View Answers of other members, check for collisions! - [x] View own reviews and score - - [ ] Mark submission as final + - [x] Mark submission as final - **Nice to have** - **Auth** - [ ] Login with LDAP @@ -46,6 +46,8 @@ - [ ] Copy team from different hunt - **General** - [x] Pretty slugified urls + - [ ] All static texts translated + - [ ] Pretty - [ ] SEO, Meta, Preview - [ ] Deployment `.on.hs-bremen.de`?? diff --git a/app/components/QuestTags.vue b/app/components/QuestTags.vue index 0d4ec72..e4a1a40 100644 --- a/app/components/QuestTags.vue +++ b/app/components/QuestTags.vue @@ -8,7 +8,7 @@ const props = defineProps<{ points: number; extraPoints: number | null; answer?: { - score: number | null; + review?: { score: number | null } | null; } | null; }; }>(); @@ -18,8 +18,8 @@ const props = defineProps<{ {{ quest.pictureRequired ? '📸' : '📷' }} {{ quest.textRequired ? '❓' : '❔' }} {{ quest.title }} - {{ quest.answer?.score || 0 }} / {{ quest.points + {{ quest.answer?.review?.score || 0 }} / {{ quest.points }}{{ quest.extraPoints ? `+${quest.extraPoints}` : '' }} { props.answer.picture?.url ? '📸✅' : '📷❌', props.answer.text ? '📝✅' : '📝❌', props.answer.review ? '🔍✅' : '🔍❌', - props.answer.score ?? 'No points', + props.answer.review?.score ?? 'No points', d(props.answer.updatedAt, { dateStyle: 'medium', timeStyle: 'medium' }), - props.answer.rankingValue, - props.answer.showcase ? '★' : null + props.answer.review?.rankingValue, + props.answer.review?.showcase ? '★' : null ]; return parts.filter((s) => !!s).join(' – '); }); const style = computed<[string, string]>(() => { - if (props.answer.review && props.answer.score !== null) { + if (props.answer.review?.review && props.answer.review?.score !== null) { return ['success', 'check']; } - if (props.answer.review || props.answer.score !== null) { + if (!props.answer.final) { + return ['', '']; + } + if (props.answer.review?.review || props.answer.review?.score !== null) { return ['danger', 'priority_high']; } if ( @@ -71,6 +77,7 @@ const style = computed<[string, string]>(() => {
+

NOT FINAL

{{ answer.picture?.url ? '📸✅ Picture done' : '📷❌ Picture missing' @@ -80,14 +87,14 @@ const style = computed<[string, string]>(() => {

{{ answer.review ? '🔍✅ Review done' : '🔍❌ Review missing' }}

-

+

- {{ answer.rankingValue }} + {{ answer.review?.rankingValue }}

-

+

Showcase

-

{{ answer.score ?? 'No' }} points

+

{{ answer.review?.score ?? 'No' }} points

(); watchDebounced( search, (newSearch) => { - if (newSearch && newSearch.length >= 3) { + if (newSearch) { safeSearch.value = newSearch; } }, diff --git a/app/pages/hunt/[huntSlug]/[questSlug]/index.vue b/app/pages/hunt/[huntSlug]/[questSlug]/index.vue index 6882ea8..5a62248 100644 --- a/app/pages/hunt/[huntSlug]/[questSlug]/index.vue +++ b/app/pages/hunt/[huntSlug]/[questSlug]/index.vue @@ -33,6 +33,7 @@ useHead({ }); const answer = ref(data.value?.answer?.text); +const final = ref(data.value?.answer?.final || false); const textType = { TEXT: 'text', @@ -44,8 +45,10 @@ const textType = { const newImage = ref(); const uploading = ref(false); +const submitError = ref(''); async function submit() { uploading.value = true; + submitError.value = ''; const body = new FormData(); @@ -55,21 +58,38 @@ async function submit() { if (answer.value) { body.set('answer', answer.value); } + if (final.value) { + body.set('final', 'true'); + } if (newImage.value) { body.set('image', newImage.value); } - const result = await $fetch(`/api/quest/${questSlug?.id}/submit`, { - method: 'POST', - body - }); + try { + await $fetch(`/api/quest/${questSlug?.id}/submit`, { + method: 'POST', + body + }); - console.log(result); - await refresh(); + await refresh(); + answer.value = data.value?.answer?.text; + newImage.value = undefined; + } catch (e) { + if (e instanceof Error && 'statusCode' in e && e.statusCode === 418) { + submitError.value = 'Answer is already finalized, no update allowed!'; + await refresh(); + } else if ( + e instanceof Error && + 'statusCode' in e && + e.statusCode === 409 + ) { + submitError.value = 'Conflict: Please refresh'; + } else { + submitError.value = parseError(e); + } + } uploading.value = false; - answer.value = data.value?.answer?.text; - newImage.value = undefined; } const createImageURL = (file: File) => URL.createObjectURL(file); @@ -83,7 +103,10 @@ const createImageURL = (file: File) => URL.createObjectURL(file); >Back to Hunt URL.createObjectURL(file); @@ -132,26 +157,34 @@ const createImageURL = (file: File) => URL.createObjectURL(file); Review -

- Answer type: {{ data.answer.score }} - - by {{ data.answer.reviewer.name }} + Score: + {{ data.answer.review.score }} + + by {{ data.answer.review.reviewer.name }} at + {{ + $d(data.answer.updatedAt, { + dateStyle: 'medium', + timeStyle: 'medium' + }) + }}

-

{{ data.answer.review }}

+

{{ data.answer.review.review }}

- + Submission
URL.createObjectURL(file); > @@ -206,13 +238,35 @@ const createImageURL = (file: File) => URL.createObjectURL(file); }} by {{ data.answer.member.name }}

+ + + {{ $t(submitError) }} Update +

+ Your final answer has already been submitted! +

diff --git a/app/pages/hunt/[huntSlug]/admin.vue b/app/pages/hunt/[huntSlug]/admin.vue index d030ecb..0f9bbdd 100644 --- a/app/pages/hunt/[huntSlug]/admin.vue +++ b/app/pages/hunt/[huntSlug]/admin.vue @@ -55,7 +55,7 @@ const newUser = ref(); watchDebounced( search, (newSearch) => { - if (newSearch && newSearch.length >= 3) { + if (newSearch) { safeSearch.value = newSearch; } }, diff --git a/app/pages/hunt/[huntSlug]/index.vue b/app/pages/hunt/[huntSlug]/index.vue index 96dc4e0..c5433ac 100644 --- a/app/pages/hunt/[huntSlug]/index.vue +++ b/app/pages/hunt/[huntSlug]/index.vue @@ -143,9 +143,14 @@ async function invite() { v-for="quest in data.quests" :key="`q-${quest.id}`" :to="$localePath(`/hunt/${sluggy(data)}/${sluggy(quest)}`)" - :stripe="quest.answer !== null" + stripe :stripe-color=" - quest.answer?.score && quest.answer?.score > 0 ? 'success' : 'primary' + (quest.answer?.review?.score && quest.answer?.review?.score > 0) || + quest.answer?.final + ? 'success' + : !!quest.answer + ? 'warning' + : 'primary' " > @@ -172,7 +177,7 @@ async function invite() { AdminAdmin ({{ quest.unreviewed || 0 }} unreviewed)
diff --git a/prisma/migrations/20250813195841_move_review_to_seperate_model/migration.sql b/prisma/migrations/20250813195841_move_review_to_seperate_model/migration.sql new file mode 100644 index 0000000..362dd51 --- /dev/null +++ b/prisma/migrations/20250813195841_move_review_to_seperate_model/migration.sql @@ -0,0 +1,62 @@ +-- CreateTable +CREATE TABLE "public"."Review" +( + "id" SERIAL NOT NULL, + "score" SMALLINT, + "review" TEXT, + "internalNote" TEXT, + "rankingValue" INTEGER, + "showcase" BOOLEAN NOT NULL DEFAULT false, + "answerId" INTEGER NOT NULL, + "reviewerId" INTEGER NOT NULL, + "createdAt" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMPTZ(3) NOT NULL, + + CONSTRAINT "Review_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "Review_answerId_key" ON "public"."Review" ("answerId"); + +-- AddForeignKey +ALTER TABLE "public"."Review" + ADD CONSTRAINT "Review_answerId_fkey" FOREIGN KEY ("answerId") REFERENCES "public"."QuestAnswer" ("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "public"."Review" + ADD CONSTRAINT "Review_reviewerId_fkey" FOREIGN KEY ("reviewerId") REFERENCES "public"."User" ("id") ON DELETE CASCADE ON UPDATE CASCADE; + +insert into public."Review" ("internalNote", + "rankingValue", + review, + score, + showcase, + "reviewerId", + "answerId", + "updatedAt", + "createdAt") +select "internalNote", + "rankingValue", + review, + score, + showcase, + "reviewerId", + id, + "updatedAt", + "updatedAt" +from "QuestAnswer" +where "reviewerId" is not null; + +-- DropForeignKey +ALTER TABLE "public"."QuestAnswer" + DROP CONSTRAINT "QuestAnswer_reviewerId_fkey"; + +-- AlterTable +ALTER TABLE "public"."QuestAnswer" + DROP COLUMN "internalNote", + DROP COLUMN "rankingValue", + DROP COLUMN "review", + DROP COLUMN "reviewerId", + DROP COLUMN "score", + DROP COLUMN "showcase", + ADD COLUMN "final" BOOLEAN NOT NULL DEFAULT false; \ No newline at end of file diff --git a/prisma/schema.prisma b/prisma/schema.prisma index f7c48d4..732ab9f 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -28,7 +28,7 @@ model User { teamsOwner HuntTeam[] teamsMember TeamMember[] answers QuestAnswer[] @relation("AnswerMember") - reviews QuestAnswer[] @relation("AnswerReview") + reviews Review[] uploadedFiles File[] } @@ -192,6 +192,7 @@ model QuestAnswer { location String? lat Decimal? @db.Decimal(10, 8) lon Decimal? @db.Decimal(10, 8) + final Boolean @default(false) team HuntTeam @relation(fields: [teamId], references: [id], onUpdate: Cascade, onDelete: Cascade) teamId Int @@ -208,8 +209,17 @@ model QuestAnswer { createdAt DateTime @default(now()) @db.Timestamptz(3) updatedAt DateTime @updatedAt @db.Timestamptz(3) - reviewer User? @relation("AnswerReview", fields: [reviewerId], references: [id], onUpdate: Cascade, onDelete: Cascade) - reviewerId Int? + review Review? + + oldFiles File[] @relation("OldAnswerFiles") + + @@index([teamId]) + @@index([memberId]) + @@index([questId]) +} + +model Review { + id Int @id @default(autoincrement()) /// Final score for this answer including bonus score Int? @db.SmallInt @@ -222,9 +232,12 @@ model QuestAnswer { /// If true, this answer will be featured in the end showcase showcase Boolean @default(false) - oldFiles File[] @relation("OldAnswerFiles") + answer QuestAnswer @relation(fields: [answerId], references: [id], onUpdate: Cascade, onDelete: Cascade) + answerId Int @unique - @@index([teamId]) - @@index([memberId]) - @@index([questId]) + reviewer User @relation(fields: [reviewerId], references: [id], onUpdate: Cascade, onDelete: Cascade) + reviewerId Int + + createdAt DateTime @default(now()) @db.Timestamptz(3) + updatedAt DateTime @updatedAt @db.Timestamptz(3) } diff --git a/prisma/seed/hunt.ts b/prisma/seed/hunt.ts index e621e74..bea2fb2 100644 --- a/prisma/seed/hunt.ts +++ b/prisma/seed/hunt.ts @@ -236,12 +236,47 @@ async function main() { const team3 = hunt.teams[2]; const images = [ - '/img/dummy/moin1.jpeg', - '/img/dummy/musikanten1.jpg', '/img/dummy/aussicht1.jpeg', 'https://pichunt.s3.swarm.syma.dev/0197ff6a-ce49-7000-a5b0-f32f1ee1b4ee', 'https://pichunt.s3.swarm.syma.dev/0197ff1a-3c8e-7000-8ba6-fa9c06548f82', - 'https://pichunt.s3.swarm.syma.dev/0197ff5b-f74d-7000-864c-20f531481fcc' + 'https://pichunt.s3.swarm.syma.dev/0197ff5b-f74d-7000-864c-20f531481fcc', + '/img/dummy/moin1.jpeg', + '/img/dummy/musikanten1.jpg' + ]; + const reviews = [ + { + reviewerId: admin.id, + score: 10, + review: 'Schönes Bild!', + internalNote: 'Wow!' + }, + { + score: 17, + reviewerId: huntMember.id, + review: 'Höchstes Team, aber leider gabs andere im AB!', + rankingValue: 10 + }, + { + score: 13, + reviewerId: admin.id, + review: 'Ihr wart 612m weit weg!', + rankingValue: 612, + internalNote: '612m' + }, + { + score: 21, + reviewerId: admin.id, + review: 'Ihr wart 2110m weit weg!', + rankingValue: 2110, + internalNote: 'Bisher am weitesten' + }, + { + score: 18, + reviewerId: huntMember.id, + review: 'Ihr wart 945m weit weg!', + rankingValue: 945, + showcase: true + } ]; const answers = await tx.questAnswer.createManyAndReturn({ data: [ @@ -253,10 +288,35 @@ async function main() { location: 'An der Haltestelle Herdentor', lat: 53.079138, lon: 8.809734, - reviewerId: admin.id, - score: 10, - review: 'Schönes Bild!', - internalNote: 'Wow!' + final: true + }, + { + teamId: team.id, + memberId: teamMember.id, + questId: hunt.quests[2].id, + text: 'Neustadtswall AB-Gebäude Etage 10', + final: true + }, + { + teamId: team.id, + memberId: teamMember.id, + questId: hunt.quests[3].id, + text: 'Theater am Leibnizplatz', + final: true + }, + { + teamId: team2.id, + memberId: teamMember2.id, + questId: hunt.quests[3].id, + text: 'Bürgerpark', + final: true + }, + { + teamId: team3.id, + memberId: teamMember3.id, + questId: hunt.quests[3].id, + text: 'Am Brill', + final: true }, { teamId: team.id, @@ -264,52 +324,13 @@ async function main() { questId: hunt.quests[1].id, text: '14', location: 'Marktplatz', - score: 10 - }, - { - teamId: team.id, - memberId: teamMember.id, - questId: hunt.quests[2].id, - text: 'Neustadtswall AB-Gebäude Etage 10', - score: 17, - reviewerId: huntMember.id, - review: 'Höchstes Team, aber leider gabs andere im AB!', - rankingValue: 10 - }, - { - teamId: team.id, - memberId: teamMember.id, - questId: hunt.quests[3].id, - text: 'Theater am Leibnizplatz', - score: 13, - reviewerId: admin.id, - review: 'Ihr wart 612m weit weg!', - rankingValue: 612, - internalNote: '612m' - }, - { - teamId: team2.id, - memberId: teamMember2.id, - questId: hunt.quests[3].id, - text: 'Bürgerpark', - score: 21, - reviewerId: admin.id, - review: 'Ihr wart 2110m weit weg!', - rankingValue: 2110, - internalNote: 'Bisher am weitesten' - }, - { - teamId: team3.id, - memberId: teamMember3.id, - questId: hunt.quests[3].id, - text: 'Am Brill', - score: 18, - reviewerId: huntMember.id, - review: 'Ihr wart 945m weit weg!', - rankingValue: 945, - showcase: true + final: false } - ] + ], + select: { + id: true, + memberId: true + } }); await Promise.all( @@ -325,6 +346,16 @@ async function main() { }) ) ); + await Promise.all( + reviews.map((r, i) => + tx.review.create({ + data: { + answerId: answers[i].id, + ...r + } + }) + ) + ); console.log(JSON.stringify(hunt, null, 2)); }); diff --git a/server/api/admin/answer/[answerId]/review.post.ts b/server/api/admin/answer/[answerId]/review.post.ts index 1c2dd0a..eb0f1ab 100644 --- a/server/api/admin/answer/[answerId]/review.post.ts +++ b/server/api/admin/answer/[answerId]/review.post.ts @@ -45,23 +45,30 @@ export default defineEventHandler(async (event) => { if (!answer) { throw createError({ status: 404, statusText: 'Not found!' }); } - const { score, review } = await useValidatedBody( + const data = await useValidatedBody( event, z.object({ score: z.int().min(0).max(32767).nullable(), - review: z.string().max(256).nullable() + review: z.string().max(256).nullable(), + showcase: z.boolean().nullable().default(false), + internalNote: z.string().max(256).nullable(), + rankingValue: z.int().nullable() }) ); - await prisma.questAnswer.update({ + await prisma.review.upsert({ where: { - id: answerId + answerId }, - data: { - score, - review, + update: { + ...data, + reviewerId: userId + }, + create: { + ...data, reviewerId: userId } }); + return answer; }); diff --git a/server/api/admin/hunt/[huntId]/showcase.get.ts b/server/api/admin/hunt/[huntId]/showcase.get.ts index b3a64f0..155238b 100644 --- a/server/api/admin/hunt/[huntId]/showcase.get.ts +++ b/server/api/admin/hunt/[huntId]/showcase.get.ts @@ -56,7 +56,9 @@ export default defineEventHandler(async (event) => { members: true, answers: { where: { - score: { gt: 0 } + review: { + score: { gt: 0 } + } } } } @@ -66,10 +68,16 @@ export default defineEventHandler(async (event) => { description: true, answers: { select: { - score: true + review: { + select: { + score: true + } + } }, where: { - score: { not: null } + review: { + score: { not: null } + } } } } @@ -88,7 +96,11 @@ export default defineEventHandler(async (event) => { select: { id: true, text: true, - score: true, + review: { + select: { + score: true + } + }, picture: { select: { @@ -110,11 +122,21 @@ export default defineEventHandler(async (event) => { } }, where: { - showcase: true + review: { + showcase: true + } }, orderBy: [ - { rankingValue: { sort: 'asc', nulls: 'last' } }, - { score: { sort: 'asc', nulls: 'last' } }, + { + review: { + rankingValue: { sort: 'asc', nulls: 'last' } + } + }, + { + review: { + score: { sort: 'asc', nulls: 'last' } + } + }, { createdAt: 'asc' } ] } @@ -124,7 +146,9 @@ export default defineEventHandler(async (event) => { { answers: { some: { - showcase: true + review: { + showcase: true + } } } } @@ -155,7 +179,7 @@ export default defineEventHandler(async (event) => { id, description, answers: _count.answers, - score: answers.reduce((sum, a) => sum + (a.score || 0), 0) + score: answers.reduce((sum, a) => sum + (a.review?.score || 0), 0) })) .toSorted((a, b) => b.score - a.score); diff --git a/server/api/admin/hunt/[huntId]/teamless.get.ts b/server/api/admin/hunt/[huntId]/teamless.get.ts index 04f9d10..1d8ee36 100644 --- a/server/api/admin/hunt/[huntId]/teamless.get.ts +++ b/server/api/admin/hunt/[huntId]/teamless.get.ts @@ -43,7 +43,7 @@ export default defineEventHandler(async (event) => { const { email } = await useValidatedQuery( event, z.object({ - email: z.string().min(3).trim() + email: z.string().trim().default('') }) ); diff --git a/server/api/admin/quest/[questId]/index.get.ts b/server/api/admin/quest/[questId]/index.get.ts index d5fb804..f0b66a6 100644 --- a/server/api/admin/quest/[questId]/index.get.ts +++ b/server/api/admin/quest/[questId]/index.get.ts @@ -51,6 +51,7 @@ export default defineEventHandler(async (event) => { select: { id: true, text: true, + final: true, updatedAt: true, team: { select: { @@ -58,15 +59,19 @@ export default defineEventHandler(async (event) => { name: true } }, - score: true, - review: true, - internalNote: true, - rankingValue: true, - showcase: true, - reviewer: { + review: { select: { - name: true, - id: true + score: true, + review: true, + internalNote: true, + rankingValue: true, + showcase: true, + reviewer: { + select: { + name: true, + id: true + } + } } }, member: { @@ -83,8 +88,9 @@ export default defineEventHandler(async (event) => { } }, orderBy: [ - { rankingValue: { sort: 'desc', nulls: 'last' } }, - { score: { sort: 'desc', nulls: 'last' } }, + { final: 'desc' }, + { review: { rankingValue: { sort: 'desc', nulls: 'last' } } }, + { review: { score: { sort: 'desc', nulls: 'last' } } }, { updatedAt: 'desc' } diff --git a/server/api/hunt/[huntId]/index.get.ts b/server/api/hunt/[huntId]/index.get.ts index 7f18560..5e5aa12 100644 --- a/server/api/hunt/[huntId]/index.get.ts +++ b/server/api/hunt/[huntId]/index.get.ts @@ -96,20 +96,42 @@ export default defineEventHandler(async (event) => { textUnit: true, _count: { select: { - answers: true + answers: { + where: { + final: true, + OR: [ + { + review: null + }, + { + review: { score: null } + }, + { + review: { + review: null + } + } + ] + } + } } }, answers: { select: { id: true, text: true, + final: true, picture: { select: { url: true } }, - score: true, - review: true, + review: { + select: { + score: true, + review: true + } + }, member: { select: { name: true @@ -160,13 +182,14 @@ export default defineEventHandler(async (event) => { const isMember = hunt.creator.id === userId || members.length > 0; const ownTeam = teams.length > 0 ? teams[0] : null; - const mappedQuests = quests.map(({ answers, ...q }) => ({ + const mappedQuests = quests.map(({ answers, _count, ...q }) => ({ ...q, + unreviewed: isMember ? _count.answers : undefined, answer: answers.length > 0 ? answers[0] : null })); const totalScore = mappedQuests.reduce( - (acc, q) => acc + (q.answer?.score ?? 0), + (acc, q) => acc + (q.answer?.review?.score ?? 0), 0 ); @@ -176,7 +199,7 @@ export default defineEventHandler(async (event) => { ownTeam, loggedIn: !!user.user, totalScore: ownTeam !== null ? totalScore : undefined, - quests: hunt.revealQuests ? mappedQuests : null, + quests: hunt.revealQuests || isMember ? mappedQuests : null, ...hunt }; }); diff --git a/server/api/quest/[questId]/index.get.ts b/server/api/quest/[questId]/index.get.ts index aa7e3ec..8df37a2 100644 --- a/server/api/quest/[questId]/index.get.ts +++ b/server/api/quest/[questId]/index.get.ts @@ -50,18 +50,24 @@ export default defineEventHandler(async (event) => { url: true } }, - score: true, - review: true, + review: { + select: { + score: true, + review: true, + reviewer: { + select: { + name: true + } + }, + updatedAt: true + } + }, member: { select: { name: true } }, - reviewer: { - select: { - name: true - } - }, + final: true, createdAt: true, updatedAt: true }, diff --git a/server/api/quest/[questId]/submit.post.ts b/server/api/quest/[questId]/submit.post.ts index a6a0cb0..a901a5d 100644 --- a/server/api/quest/[questId]/submit.post.ts +++ b/server/api/quest/[questId]/submit.post.ts @@ -46,7 +46,8 @@ export default defineEventHandler(async (event) => { id: true, text: true, picture: true, - updatedAt: true + updatedAt: true, + final: true }, where: { team: { @@ -75,6 +76,9 @@ export default defineEventHandler(async (event) => { if (quest.hunt.teams.length <= 0) { throw createError({ status: 403, statusText: 'No team yet!' }); } + if (quest.answers.length > 0 && quest.answers[0].final) { + throw createError({ status: 418, statusText: 'Already marked as final!' }); + } const team = quest.hunt.teams[0]; @@ -89,7 +93,8 @@ export default defineEventHandler(async (event) => { .file() .mime(['image/png', 'image/jpeg', 'image/jpg']) .max(10_000_000) - .optional() + .optional(), + final: zh.boolAsString.optional() }) .parse(formDataObj); @@ -113,7 +118,6 @@ export default defineEventHandler(async (event) => { }); } - // TODO: prevent updating reviewed answers // TODO: respect end of hunt let picture: { creatorId: number; url: string; uuid: string } | undefined = @@ -155,7 +159,8 @@ export default defineEventHandler(async (event) => { data: { memberId: userId, text: data.answer, - pictureId: file.id + pictureId: file.id, + final: data.final } }); } else { @@ -164,7 +169,8 @@ export default defineEventHandler(async (event) => { teamId: team.id, memberId: userId, questId: quest.id, - text: data.answer + text: data.answer, + final: data.final }, select: { id: true } }); @@ -187,7 +193,8 @@ export default defineEventHandler(async (event) => { }, data: { memberId: userId, - text: data.answer + text: data.answer, + final: data.final } }); } else { @@ -196,7 +203,8 @@ export default defineEventHandler(async (event) => { teamId: team.id, memberId: userId, questId: quest.id, - text: data.answer + text: data.answer, + final: data.final } }); }