diff --git a/app/admin/guests/[id]/page.tsx b/app/admin/guests/[id]/page.tsx index 44025a0..878cb32 100644 --- a/app/admin/guests/[id]/page.tsx +++ b/app/admin/guests/[id]/page.tsx @@ -3,6 +3,7 @@ import { notFound } from "next/navigation" import { CopyInviteButton } from "@/components/admin/copy-invite-button" import { DeleteInvitationDialog } from "@/components/admin/delete-invitation-dialog" +import { InvitationLanguageSelect } from "@/components/admin/invitation-language-select" import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert" import { Badge } from "@/components/ui/badge" import { Button } from "@/components/ui/button" @@ -146,6 +147,14 @@ export default async function GuestDetailPage({ Admin-managed invitation fields. +
+ Language + +
Contact email {detail.invitation.contact_email ?? "—"} diff --git a/app/admin/guests/actions.ts b/app/admin/guests/actions.ts index ead413c..58dc2de 100644 --- a/app/admin/guests/actions.ts +++ b/app/admin/guests/actions.ts @@ -4,14 +4,25 @@ import { revalidatePath } from "next/cache" import { auth } from "@/auth" import { isAdminEmail } from "@/lib/admin-auth" -import { updateSaveTheDateSent } from "@/lib/db/queries/invitations" -import { updateSaveTheDateSentSchema } from "@/lib/validation/invitation" +import { + updateInvitationLanguage, + updateSaveTheDateSent, +} from "@/lib/db/queries/invitations" +import { + updateInvitationLanguageSchema, + updateSaveTheDateSentSchema, +} from "@/lib/validation/invitation" export interface UpdateSaveTheDateSentResult { ok: boolean error: string | null } +export interface UpdateInvitationLanguageResult { + ok: boolean + error: string | null +} + export async function updateSaveTheDateSentAction(input: { invitationId: string sent: boolean @@ -49,3 +60,42 @@ export async function updateSaveTheDateSentAction(input: { return { ok: true, error: null } } + +export async function updateInvitationLanguageAction(input: { + invitationId: string + language: string +}): Promise { + const session = await auth() + const adminEmail = session?.user?.email + + if (!isAdminEmail(adminEmail)) { + return { ok: false, error: "You are not allowed to update invitations." } + } + + const parsed = updateInvitationLanguageSchema.safeParse(input) + + if (!parsed.success) { + return { ok: false, error: "Select a supported language." } + } + + try { + const updated = await updateInvitationLanguage({ + invitationId: parsed.data.invitationId, + language: parsed.data.language, + adminEmail: adminEmail ?? "", + }) + + if (!updated) { + return { ok: false, error: "This guest group no longer exists." } + } + } catch (error) { + console.error("Failed to update invitation language", error) + return { ok: false, error: "Could not update the invitation language." } + } + + revalidatePath(`/admin/guests/${parsed.data.invitationId}`) + revalidatePath("/admin/guests") + revalidatePath("/admin") + + return { ok: true, error: null } +} diff --git a/components/admin/invitation-language-select.tsx b/components/admin/invitation-language-select.tsx new file mode 100644 index 0000000..b59cf4b --- /dev/null +++ b/components/admin/invitation-language-select.tsx @@ -0,0 +1,90 @@ +"use client" + +import { useRouter } from "next/navigation" +import { useState } from "react" + +import { updateInvitationLanguageAction } from "@/app/admin/guests/actions" +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select" +import { supportedLanguages } from "@/lib/i18n/config" + +interface InvitationLanguageSelectProps { + invitationId: string + currentLanguage: string + displayName: string +} + +export function InvitationLanguageSelect({ + invitationId, + currentLanguage, + displayName, +}: InvitationLanguageSelectProps) { + const router = useRouter() + const [value, setValue] = useState(currentLanguage) + const [message, setMessage] = useState(null) + const [error, setError] = useState(null) + const [isSaving, setIsSaving] = useState(false) + + async function updateLanguage(nextLanguage: string) { + if (nextLanguage === value) return + + const previousLanguage = value + + setValue(nextLanguage) + setMessage(null) + setError(null) + setIsSaving(true) + + try { + const result = await updateInvitationLanguageAction({ + invitationId, + language: nextLanguage, + }) + + if (!result.ok) { + setValue(previousLanguage) + setError(result.error) + return + } + + setMessage("Language updated.") + router.refresh() + } catch { + setValue(previousLanguage) + setError("Could not update the invitation language.") + } finally { + setIsSaving(false) + } + } + + return ( +
+ + {message ?

{message}

: null} + {error ?

{error}

: null} +
+ ) +} diff --git a/lib/db/queries/invitations.ts b/lib/db/queries/invitations.ts index 3e7b3db..3bed4c4 100644 --- a/lib/db/queries/invitations.ts +++ b/lib/db/queries/invitations.ts @@ -191,6 +191,55 @@ export async function updateSaveTheDateSent({ }) } +export async function updateInvitationLanguage({ + invitationId, + language, + adminEmail, +}: { + invitationId: string + language: string + adminEmail: string +}) { + return db.transaction().execute(async (trx) => { + const invitation = await trx + .selectFrom("invitations") + .select(["id", "display_name", "language"]) + .where("id", "=", invitationId) + .executeTakeFirst() + + if (!invitation) return null + + const updated = await trx + .updateTable("invitations") + .set({ + language, + updated_at: new Date(), + }) + .where("id", "=", invitationId) + .returning(["id", "language"]) + .executeTakeFirst() + + if (!updated) return null + + await trx + .insertInto("admin_audit_events") + .values({ + admin_email: adminEmail, + action: "invitation.language_updated", + target_type: "invitation", + target_id: invitationId, + metadata: { + displayName: invitation.display_name, + previousLanguage: invitation.language, + newLanguage: language, + }, + }) + .execute() + + return updated + }) +} + export async function listInvitations(): Promise { const invitations = await db .selectFrom("invitations") diff --git a/lib/validation/invitation.ts b/lib/validation/invitation.ts index bcc9bc9..0ee5ec2 100644 --- a/lib/validation/invitation.ts +++ b/lib/validation/invitation.ts @@ -54,6 +54,14 @@ export const updateSaveTheDateSentSchema = z.object({ sent: z.boolean(), }) +export const updateInvitationLanguageSchema = z.object({ + invitationId: z.uuid(), + language: z.enum(supportedLanguageCodes, { + error: "Select a supported language", + }), +}) + export type CreateInvitationInput = z.infer export type DeleteInvitationInput = z.infer export type UpdateSaveTheDateSentInput = z.infer +export type UpdateInvitationLanguageInput = z.infer