This commit is contained in:
@@ -0,0 +1,56 @@
|
||||
import { db } from "@/lib/db/client"
|
||||
import type { ToastRequest } from "@/lib/db/types"
|
||||
import type { InteractionSubmissionInput, ToastStatus } from "@/lib/validation/interactions"
|
||||
|
||||
export interface InvitationInteractions {
|
||||
toastRequests: ToastRequest[]
|
||||
}
|
||||
|
||||
export async function getInvitationInteractions(
|
||||
invitationId: string,
|
||||
): Promise<InvitationInteractions> {
|
||||
const toastRequests = await db
|
||||
.selectFrom("toast_requests")
|
||||
.selectAll()
|
||||
.where("invitation_id", "=", invitationId)
|
||||
.orderBy("created_at", "asc")
|
||||
.execute()
|
||||
|
||||
return { toastRequests }
|
||||
}
|
||||
|
||||
export async function replaceInvitationInteractions(input: InteractionSubmissionInput) {
|
||||
return db.transaction().execute(async (trx) => {
|
||||
await trx
|
||||
.deleteFrom("toast_requests")
|
||||
.where("invitation_id", "=", input.invitationId)
|
||||
.execute()
|
||||
|
||||
if (input.toastRequest) {
|
||||
await trx
|
||||
.insertInto("toast_requests")
|
||||
.values({
|
||||
invitation_id: input.invitationId,
|
||||
member_id: input.toastRequest.memberId,
|
||||
speaker_name: input.toastRequest.speakerName,
|
||||
estimated_minutes: input.toastRequest.estimatedMinutes,
|
||||
status: "pending",
|
||||
})
|
||||
.execute()
|
||||
}
|
||||
|
||||
await trx
|
||||
.updateTable("invitations")
|
||||
.set({ updated_at: new Date() })
|
||||
.where("id", "=", input.invitationId)
|
||||
.execute()
|
||||
})
|
||||
}
|
||||
|
||||
export async function updateToastRequestStatus(id: string, status: ToastStatus) {
|
||||
await db
|
||||
.updateTable("toast_requests")
|
||||
.set({ status, updated_at: new Date() })
|
||||
.where("id", "=", id)
|
||||
.execute()
|
||||
}
|
||||
@@ -0,0 +1,260 @@
|
||||
import { nanoid } from "nanoid"
|
||||
|
||||
import { db } from "@/lib/db/client"
|
||||
import type { Invitation, InvitationMember, InvitationResponse } from "@/lib/db/types"
|
||||
import type { CreateInvitationInput } from "@/lib/validation/invitation"
|
||||
import type { SubmitInvitationResponseInput } from "@/lib/validation/response"
|
||||
|
||||
export type DerivedInvitationStatus = "Pending" | "Partially answered" | "Attending" | "Declined" | "Partial"
|
||||
|
||||
export interface InvitationListItem {
|
||||
id: string
|
||||
token: string
|
||||
displayName: string
|
||||
language: string
|
||||
contactEmail: string | null
|
||||
contactPhone: string | null
|
||||
maxPartySize: number
|
||||
allowMemberEdits: boolean
|
||||
membersCount: number
|
||||
attendingCount: number
|
||||
pendingCount: number
|
||||
derivedStatus: DerivedInvitationStatus
|
||||
updatedAt: Date
|
||||
}
|
||||
|
||||
export interface InvitationDetail {
|
||||
invitation: Invitation
|
||||
members: InvitationMember[]
|
||||
response: InvitationResponse | null
|
||||
derivedStatus: DerivedInvitationStatus
|
||||
}
|
||||
|
||||
function deriveStatus(members: InvitationMember[], submittedAt?: Date | null): DerivedInvitationStatus {
|
||||
if (!submittedAt) return "Pending"
|
||||
|
||||
const pendingCount = members.filter((member) => member.attending === null).length
|
||||
const attendingCount = members.filter((member) => member.attending === true).length
|
||||
const declinedCount = members.filter((member) => member.attending === false).length
|
||||
|
||||
if (pendingCount > 0) return "Partially answered"
|
||||
if (attendingCount > 0 && declinedCount > 0) return "Partial"
|
||||
if (attendingCount > 0) return "Attending"
|
||||
return "Declined"
|
||||
}
|
||||
|
||||
export async function createInvitationWithMembers(input: CreateInvitationInput) {
|
||||
return db.transaction().execute(async (trx) => {
|
||||
const invitation = await trx
|
||||
.insertInto("invitations")
|
||||
.values({
|
||||
token: nanoid(32),
|
||||
display_name: input.displayName,
|
||||
language: input.language,
|
||||
contact_email: input.contactEmail,
|
||||
contact_phone: input.contactPhone,
|
||||
max_party_size: input.maxPartySize,
|
||||
allow_member_edits: input.allowMemberEdits,
|
||||
notes: input.notes,
|
||||
})
|
||||
.returningAll()
|
||||
.executeTakeFirstOrThrow()
|
||||
|
||||
await trx
|
||||
.insertInto("invitation_members")
|
||||
.values(
|
||||
input.members.map((name, index) => ({
|
||||
invitation_id: invitation.id,
|
||||
name,
|
||||
is_primary_contact: index === 0,
|
||||
})),
|
||||
)
|
||||
.execute()
|
||||
|
||||
return invitation
|
||||
})
|
||||
}
|
||||
|
||||
export async function listInvitations(): Promise<InvitationListItem[]> {
|
||||
const invitations = await db
|
||||
.selectFrom("invitations")
|
||||
.selectAll()
|
||||
.orderBy("updated_at", "desc")
|
||||
.execute()
|
||||
|
||||
if (invitations.length === 0) return []
|
||||
|
||||
const invitationIds = invitations.map((invitation) => invitation.id)
|
||||
|
||||
const [members, responses] = await Promise.all([
|
||||
db
|
||||
.selectFrom("invitation_members")
|
||||
.selectAll()
|
||||
.where("invitation_id", "in", invitationIds)
|
||||
.execute(),
|
||||
db
|
||||
.selectFrom("invitation_responses")
|
||||
.selectAll()
|
||||
.where("invitation_id", "in", invitationIds)
|
||||
.execute(),
|
||||
])
|
||||
|
||||
return invitations.map((invitation) => {
|
||||
const invitationMembers = members.filter(
|
||||
(member) => member.invitation_id === invitation.id,
|
||||
)
|
||||
const response = responses.find(
|
||||
(invitationResponse) => invitationResponse.invitation_id === invitation.id,
|
||||
)
|
||||
|
||||
return {
|
||||
id: invitation.id,
|
||||
token: invitation.token,
|
||||
displayName: invitation.display_name,
|
||||
language: invitation.language,
|
||||
contactEmail: invitation.contact_email,
|
||||
contactPhone: invitation.contact_phone,
|
||||
maxPartySize: invitation.max_party_size,
|
||||
allowMemberEdits: invitation.allow_member_edits,
|
||||
membersCount: invitationMembers.length,
|
||||
attendingCount: invitationMembers.filter((member) => member.attending === true).length,
|
||||
pendingCount: invitationMembers.filter((member) => member.attending === null).length,
|
||||
derivedStatus: deriveStatus(invitationMembers, response?.submitted_at ?? null),
|
||||
updatedAt: invitation.updated_at,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export async function getInvitationDetail(id: string): Promise<InvitationDetail | null> {
|
||||
const invitation = await db
|
||||
.selectFrom("invitations")
|
||||
.selectAll()
|
||||
.where("id", "=", id)
|
||||
.executeTakeFirst()
|
||||
|
||||
if (!invitation) return null
|
||||
|
||||
const [members, response] = await Promise.all([
|
||||
db
|
||||
.selectFrom("invitation_members")
|
||||
.selectAll()
|
||||
.where("invitation_id", "=", id)
|
||||
.orderBy("is_primary_contact", "desc")
|
||||
.orderBy("created_at", "asc")
|
||||
.execute(),
|
||||
db
|
||||
.selectFrom("invitation_responses")
|
||||
.selectAll()
|
||||
.where("invitation_id", "=", id)
|
||||
.executeTakeFirst(),
|
||||
])
|
||||
|
||||
return {
|
||||
invitation,
|
||||
members,
|
||||
response: response ?? null,
|
||||
derivedStatus: deriveStatus(members, response?.submitted_at ?? null),
|
||||
}
|
||||
}
|
||||
|
||||
export async function getInvitationByToken(token: string): Promise<InvitationDetail | null> {
|
||||
const invitation = await db
|
||||
.selectFrom("invitations")
|
||||
.selectAll()
|
||||
.where("token", "=", token)
|
||||
.executeTakeFirst()
|
||||
|
||||
if (!invitation) return null
|
||||
|
||||
const [members, response] = await Promise.all([
|
||||
db
|
||||
.selectFrom("invitation_members")
|
||||
.selectAll()
|
||||
.where("invitation_id", "=", invitation.id)
|
||||
.orderBy("is_primary_contact", "desc")
|
||||
.orderBy("created_at", "asc")
|
||||
.execute(),
|
||||
db
|
||||
.selectFrom("invitation_responses")
|
||||
.selectAll()
|
||||
.where("invitation_id", "=", invitation.id)
|
||||
.executeTakeFirst(),
|
||||
])
|
||||
|
||||
return {
|
||||
invitation,
|
||||
members,
|
||||
response: response ?? null,
|
||||
derivedStatus: deriveStatus(members, response?.submitted_at ?? null),
|
||||
}
|
||||
}
|
||||
|
||||
export async function submitInvitationResponse(input: SubmitInvitationResponseInput) {
|
||||
return db.transaction().execute(async (trx) => {
|
||||
const invitation = await trx
|
||||
.selectFrom("invitations")
|
||||
.select(["id"])
|
||||
.where("token", "=", input.token)
|
||||
.executeTakeFirst()
|
||||
|
||||
if (!invitation) {
|
||||
throw new Error("Invitation not found")
|
||||
}
|
||||
|
||||
const existingMembers = await trx
|
||||
.selectFrom("invitation_members")
|
||||
.select(["id"])
|
||||
.where("invitation_id", "=", invitation.id)
|
||||
.execute()
|
||||
|
||||
const existingMemberIds = new Set(existingMembers.map((member) => member.id))
|
||||
|
||||
for (const member of input.members) {
|
||||
if (!existingMemberIds.has(member.memberId)) {
|
||||
throw new Error("Invalid invitation member")
|
||||
}
|
||||
|
||||
await trx
|
||||
.updateTable("invitation_members")
|
||||
.set({
|
||||
attending: member.attending,
|
||||
meal_preference: member.attending ? member.mealPreference : null,
|
||||
dietary_restrictions: member.attending ? member.dietaryRestrictions : null,
|
||||
updated_at: new Date(),
|
||||
})
|
||||
.where("id", "=", member.memberId)
|
||||
.where("invitation_id", "=", invitation.id)
|
||||
.execute()
|
||||
}
|
||||
|
||||
await trx
|
||||
.insertInto("invitation_responses")
|
||||
.values({
|
||||
invitation_id: invitation.id,
|
||||
submitted_at: new Date(),
|
||||
arrival_date: input.arrivalDate,
|
||||
departure_date: input.departureDate,
|
||||
stay_location: input.stayLocation,
|
||||
needs_transport_help: input.needsTransportHelp,
|
||||
message_to_couple: input.messageToCouple,
|
||||
})
|
||||
.onConflict((oc) =>
|
||||
oc.column("invitation_id").doUpdateSet({
|
||||
submitted_at: new Date(),
|
||||
arrival_date: input.arrivalDate,
|
||||
departure_date: input.departureDate,
|
||||
stay_location: input.stayLocation,
|
||||
needs_transport_help: input.needsTransportHelp,
|
||||
message_to_couple: input.messageToCouple,
|
||||
updated_at: new Date(),
|
||||
}),
|
||||
)
|
||||
.execute()
|
||||
|
||||
await trx
|
||||
.updateTable("invitations")
|
||||
.set({ updated_at: new Date() })
|
||||
.where("id", "=", invitation.id)
|
||||
.execute()
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { db } from "@/lib/db/client"
|
||||
|
||||
export async function listInvitationMembers(invitationId: string) {
|
||||
return db
|
||||
.selectFrom("invitation_members")
|
||||
.selectAll()
|
||||
.where("invitation_id", "=", invitationId)
|
||||
.orderBy("is_primary_contact", "desc")
|
||||
.orderBy("created_at", "asc")
|
||||
.execute()
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import { db } from "@/lib/db/client"
|
||||
import type { PhotoUpload } from "@/lib/db/types"
|
||||
|
||||
export async function getPhotoUploadByInvitationId(
|
||||
invitationId: string,
|
||||
): Promise<PhotoUpload | null> {
|
||||
return (
|
||||
(await db
|
||||
.selectFrom("photo_uploads")
|
||||
.selectAll()
|
||||
.where("invitation_id", "=", invitationId)
|
||||
.executeTakeFirst()) ?? null
|
||||
)
|
||||
}
|
||||
|
||||
export async function upsertPhotoUpload(input: {
|
||||
invitationId: string
|
||||
bucket: string
|
||||
objectKey: string
|
||||
originalFilename: string
|
||||
contentType: string
|
||||
sizeBytes: number
|
||||
}) {
|
||||
return db
|
||||
.insertInto("photo_uploads")
|
||||
.values({
|
||||
invitation_id: input.invitationId,
|
||||
bucket: input.bucket,
|
||||
object_key: input.objectKey,
|
||||
original_filename: input.originalFilename,
|
||||
content_type: input.contentType,
|
||||
size_bytes: input.sizeBytes,
|
||||
})
|
||||
.onConflict((oc) =>
|
||||
oc.column("invitation_id").doUpdateSet({
|
||||
bucket: input.bucket,
|
||||
object_key: input.objectKey,
|
||||
original_filename: input.originalFilename,
|
||||
content_type: input.contentType,
|
||||
size_bytes: input.sizeBytes,
|
||||
updated_at: new Date(),
|
||||
}),
|
||||
)
|
||||
.returningAll()
|
||||
.executeTakeFirstOrThrow()
|
||||
}
|
||||
Reference in New Issue
Block a user