feat: add CMS inquiries and group bookings
This commit is contained in:
@@ -0,0 +1,102 @@
|
||||
import type { CollectionAfterChangeHook, CollectionConfig } from "payload";
|
||||
import { isAdmin } from "@/access";
|
||||
import { conversationReplyEmail } from "@/lib/email/conversationTemplates";
|
||||
import { sendEmail } from "@/lib/email/sendBookingEmails";
|
||||
|
||||
const sendAdminReply: CollectionAfterChangeHook = async ({ doc, operation, req }) => {
|
||||
if (operation !== "create" || doc.senderType !== "admin") return doc;
|
||||
|
||||
try {
|
||||
const conversation = await req.payload.findByID({
|
||||
collection: "conversations",
|
||||
id: typeof doc.conversation === "object" ? doc.conversation.id : doc.conversation,
|
||||
depth: 0,
|
||||
req,
|
||||
});
|
||||
const adminUser = req.user?.collection === "users" ? req.user : null;
|
||||
await sendEmail(
|
||||
conversation.email,
|
||||
conversationReplyEmail({
|
||||
name: conversation.name,
|
||||
subject: conversation.subject,
|
||||
body: doc.body,
|
||||
senderName: doc.senderName || adminUser?.name || "Anouma",
|
||||
threadKey: conversation.threadKey,
|
||||
}),
|
||||
{ replyTo: process.env.INBOUND_REPLY_TO || "reply-to@anouma.org" },
|
||||
);
|
||||
await req.payload.update({
|
||||
collection: "conversations",
|
||||
id: conversation.id,
|
||||
req,
|
||||
data: { status: "waiting", lastMessageAt: new Date().toISOString() },
|
||||
});
|
||||
} catch (err) {
|
||||
req.payload.logger.error({ err, msg: "conversation admin reply email failed" });
|
||||
}
|
||||
|
||||
return doc;
|
||||
};
|
||||
|
||||
export const ConversationMessages: CollectionConfig = {
|
||||
slug: "conversation-messages",
|
||||
labels: {
|
||||
singular: "Nachricht",
|
||||
plural: "Nachrichten",
|
||||
},
|
||||
admin: {
|
||||
useAsTitle: "body",
|
||||
defaultColumns: ["conversation", "senderType", "senderName", "createdAt"],
|
||||
group: "Kommunikation",
|
||||
description: "Einzelne Nachrichten innerhalb einer Anfrage. Antworten aus dem CMS werden automatisch per E-Mail verschickt.",
|
||||
},
|
||||
access: {
|
||||
read: isAdmin,
|
||||
create: isAdmin,
|
||||
update: isAdmin,
|
||||
delete: isAdmin,
|
||||
},
|
||||
timestamps: true,
|
||||
hooks: {
|
||||
afterChange: [sendAdminReply],
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: "conversation",
|
||||
type: "relationship",
|
||||
relationTo: "conversations",
|
||||
label: "Anfrage",
|
||||
required: true,
|
||||
index: true,
|
||||
},
|
||||
{
|
||||
name: "senderType",
|
||||
type: "select",
|
||||
label: "Absenderart",
|
||||
required: true,
|
||||
defaultValue: "admin",
|
||||
options: [
|
||||
{ label: "Anfragende Person", value: "visitor" },
|
||||
{ label: "Anouma", value: "admin" },
|
||||
],
|
||||
admin: { description: "Für Antworten im CMS „Anouma“ auswählen. Eingehende E-Mails werden automatisch als Anfragende Person gespeichert." },
|
||||
},
|
||||
{ name: "senderName", type: "text", label: "Absendername" },
|
||||
{ name: "senderEmail", type: "email", label: "Absender-E-Mail" },
|
||||
{
|
||||
name: "body",
|
||||
type: "textarea",
|
||||
label: "Nachricht",
|
||||
required: true,
|
||||
minLength: 1,
|
||||
},
|
||||
{
|
||||
name: "externalMessageId",
|
||||
type: "text",
|
||||
label: "Externe Nachrichten-ID",
|
||||
unique: true,
|
||||
index: true,
|
||||
admin: { position: "sidebar", readOnly: true },
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -0,0 +1,88 @@
|
||||
import { randomUUID } from "node:crypto";
|
||||
import type { CollectionConfig } from "payload";
|
||||
import { isAdmin, isAdminFieldLevel } from "@/access";
|
||||
|
||||
export const Conversations: CollectionConfig = {
|
||||
slug: "conversations",
|
||||
labels: {
|
||||
singular: "Anfrage",
|
||||
plural: "Anfragen",
|
||||
},
|
||||
admin: {
|
||||
useAsTitle: "subject",
|
||||
defaultColumns: ["subject", "name", "email", "status", "lastMessageAt"],
|
||||
group: "Kommunikation",
|
||||
description: "Kontakt- und Terminanfragen als durchsuchbare Konversationen.",
|
||||
},
|
||||
access: {
|
||||
read: isAdmin,
|
||||
create: () => false,
|
||||
update: isAdmin,
|
||||
delete: isAdmin,
|
||||
},
|
||||
timestamps: true,
|
||||
fields: [
|
||||
{
|
||||
name: "subject",
|
||||
type: "text",
|
||||
label: "Betreff",
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
type: "row",
|
||||
fields: [
|
||||
{ name: "name", type: "text", label: "Name", required: true, admin: { width: "50%" } },
|
||||
{ name: "email", type: "email", label: "E-Mail", required: true, admin: { width: "50%" } },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "type",
|
||||
type: "select",
|
||||
label: "Art",
|
||||
required: true,
|
||||
defaultValue: "contact",
|
||||
options: [
|
||||
{ label: "Kontakt", value: "contact" },
|
||||
{ label: "Terminanfrage", value: "appointment" },
|
||||
{ label: "Gruppenangebot", value: "group" },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "status",
|
||||
type: "select",
|
||||
label: "Status",
|
||||
required: true,
|
||||
defaultValue: "open",
|
||||
options: [
|
||||
{ label: "Offen", value: "open" },
|
||||
{ label: "Wartet auf Antwort", value: "waiting" },
|
||||
{ label: "Erledigt", value: "closed" },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "customer",
|
||||
type: "relationship",
|
||||
relationTo: "customers",
|
||||
label: "Kundenkonto",
|
||||
admin: { position: "sidebar", readOnly: true },
|
||||
},
|
||||
{
|
||||
name: "threadKey",
|
||||
type: "text",
|
||||
label: "E-Mail-Thread-Schlüssel",
|
||||
required: true,
|
||||
unique: true,
|
||||
index: true,
|
||||
defaultValue: () => randomUUID(),
|
||||
access: { read: isAdminFieldLevel, update: () => false },
|
||||
admin: { position: "sidebar", readOnly: true },
|
||||
},
|
||||
{
|
||||
name: "lastMessageAt",
|
||||
type: "date",
|
||||
label: "Letzte Nachricht",
|
||||
index: true,
|
||||
admin: { position: "sidebar", readOnly: true },
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -0,0 +1,50 @@
|
||||
import type { CollectionConfig } from "payload";
|
||||
import { isAdmin } from "@/access";
|
||||
|
||||
export const GroupRegistrations: CollectionConfig = {
|
||||
slug: "group-registrations",
|
||||
labels: {
|
||||
singular: "Gruppenanmeldung",
|
||||
plural: "Gruppenanmeldungen",
|
||||
},
|
||||
admin: {
|
||||
useAsTitle: "name",
|
||||
defaultColumns: ["name", "email", "offer", "status", "registeredAt"],
|
||||
group: "Buchungen",
|
||||
description: "Anmeldungen und Bewerbungen für Gruppenangebote.",
|
||||
},
|
||||
access: {
|
||||
read: isAdmin,
|
||||
create: () => false,
|
||||
update: isAdmin,
|
||||
delete: isAdmin,
|
||||
},
|
||||
timestamps: true,
|
||||
fields: [
|
||||
{ name: "offer", type: "relationship", relationTo: "offers", label: "Angebot", required: true, index: true },
|
||||
{
|
||||
type: "row",
|
||||
fields: [
|
||||
{ name: "name", type: "text", label: "Name", required: true, admin: { width: "50%" } },
|
||||
{ name: "email", type: "email", label: "E-Mail", required: true, admin: { width: "50%" } },
|
||||
],
|
||||
},
|
||||
{ name: "phone", type: "text", label: "Telefon" },
|
||||
{ name: "message", type: "textarea", label: "Nachricht / Motivation" },
|
||||
{
|
||||
name: "status",
|
||||
type: "select",
|
||||
label: "Status",
|
||||
required: true,
|
||||
defaultValue: "pending",
|
||||
options: [
|
||||
{ label: "Zu prüfen", value: "pending" },
|
||||
{ label: "Angenommen", value: "accepted" },
|
||||
{ label: "Warteliste", value: "waitlist" },
|
||||
{ label: "Abgelehnt", value: "rejected" },
|
||||
{ label: "Storniert", value: "cancelled" },
|
||||
],
|
||||
},
|
||||
{ name: "registeredAt", type: "date", label: "Eingegangen am", defaultValue: () => new Date().toISOString(), admin: { position: "sidebar" } },
|
||||
],
|
||||
};
|
||||
@@ -106,6 +106,72 @@ export const Offers: CollectionConfig = {
|
||||
description: "Aktiviert die Terminbuchung mit Kalenderauswahl auf der Angebotsseite.",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "bookingMode",
|
||||
type: "select",
|
||||
label: "Buchungsart",
|
||||
required: true,
|
||||
defaultValue: "individual",
|
||||
options: [
|
||||
{ label: "Einzeltermin mit Kalender", value: "individual" },
|
||||
{ label: "Gruppenangebot / Anmeldung", value: "group" },
|
||||
],
|
||||
admin: {
|
||||
position: "sidebar",
|
||||
description: "Gruppenangebote werden über Anmeldung, feste Wiederholung oder Bewerbungsfrist organisiert.",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "groupRegistrationMode",
|
||||
type: "select",
|
||||
label: "Gruppenmodus",
|
||||
defaultValue: "fixed",
|
||||
options: [
|
||||
{ label: "Fester Termin (wöchentlich/monatlich)", value: "fixed" },
|
||||
{ label: "Plätze mit Bewerbungsfrist", value: "capacity" },
|
||||
{ label: "Unverbindliche Anmeldung", value: "request" },
|
||||
],
|
||||
admin: { condition: (data) => data?.bookingMode === "group" },
|
||||
},
|
||||
{
|
||||
name: "groupSchedule",
|
||||
type: "group",
|
||||
label: "Wiederkehrender Termin",
|
||||
admin: { condition: (data) => data?.bookingMode === "group" && data?.groupRegistrationMode === "fixed" },
|
||||
fields: [
|
||||
{
|
||||
name: "recurrence",
|
||||
type: "select",
|
||||
label: "Rhythmus",
|
||||
defaultValue: "weekly",
|
||||
options: [
|
||||
{ label: "Jede Woche", value: "weekly" },
|
||||
{ label: "Jeden Monat", value: "monthly" },
|
||||
],
|
||||
},
|
||||
{ name: "weekday", type: "select", label: "Wochentag", options: [
|
||||
{ label: "Montag", value: "monday" }, { label: "Dienstag", value: "tuesday" }, { label: "Mittwoch", value: "wednesday" },
|
||||
{ label: "Donnerstag", value: "thursday" }, { label: "Freitag", value: "friday" }, { label: "Samstag", value: "saturday" }, { label: "Sonntag", value: "sunday" },
|
||||
] },
|
||||
{ name: "monthDay", type: "number", label: "Tag im Monat", min: 1, max: 31, admin: { condition: (data) => data?.recurrence === "monthly" } },
|
||||
{ name: "startTime", type: "text", label: "Beginn", admin: { description: "z. B. 18:00 Uhr" } },
|
||||
{ name: "endTime", type: "text", label: "Ende", admin: { description: "z. B. 20:00 Uhr" } },
|
||||
],
|
||||
},
|
||||
{
|
||||
type: "row",
|
||||
admin: { condition: (data) => data?.bookingMode === "group" },
|
||||
fields: [
|
||||
{ name: "groupCapacity", type: "number", label: "Plätze", min: 1, admin: { width: "50%" } },
|
||||
{ name: "registrationDeadline", type: "date", label: "Anmelde-/Bewerbungsfrist", admin: { width: "50%", date: { pickerAppearance: "dayOnly", displayFormat: "dd.MM.yyyy" } } },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "groupInfo",
|
||||
type: "textarea",
|
||||
label: "Hinweis zum Gruppenangebot",
|
||||
admin: { condition: (data) => data?.bookingMode === "group", description: "Wird auf der Angebotsseite oberhalb des Anmeldeformulars angezeigt." },
|
||||
},
|
||||
{
|
||||
type: "row",
|
||||
fields: [
|
||||
|
||||
Reference in New Issue
Block a user