mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 19:01:08 +02:00
feat(forms): M7b auto-sync zu events — RSVP-Pipeline für Anmeldungen
Erweiterung von M7a um events als zweites Auto-Sync-Target
(docs/plans/forms-module.md M7 — Teil 2):
- AutoSyncConfig erweitert mit optionalem `targetId` — speichert die
eventId, zu der RSVPs angelegt werden sollen.
- lib/auto-sync.ts:
- buildEventGuestFromAnswers (pure): mapping form-field-id →
guest-key (name/email/phone/note/plusOnes). plusOnes wird zu
non-negativem Integer gecoerct, Negative + non-numeric werden
silently gedroppt.
- dispatchTarget('events'): wirft wenn targetId fehlt; ruft
eventGuestsStore.addGuest mit rsvpStatus='yes' (Form-Submit
impliziert Zusage). Mindest-Voraussetzung: name muss gesetzt
sein, sonst null (verhindert leere Gast-Zeilen).
- feedback / library / space_member bleiben strukturell mit
"noch nicht implementiert"-throw — feedback ist eigene Domain
(kein Dexie), library + space_member brauchen mehr Architektur.
- lib/auto-sync.spec.ts: 4 neue Tests (direct mapping, plusOnes
parsing, empty/null skip, unknown-key drop). Total Forms-Tests
jetzt 26/26.
- SettingsPanel: SUPPORTED_TARGETS auf [contacts, events] erweitert.
Bei target='events': event-picker dropdown (nutzt useAllEvents),
Hint wenn kein Event gewählt, Mapping-Grid mit GuestKey-Optionen
(name, email, phone, note, Begleitpersonen). Target-Switch löscht
altes mapping (verschiedene Targets, verschiedene Keys).
- 13 neue i18n-Keys × 5 Locales (autoSync.targetEvents,
eventsHint, eventPicker*, eventNeeded, guestKey.*).
Use-Case: Vereins-Sommerfest. Form mit "Wie heißt du? / Email /
Bringst du jemanden mit?" → autoSync zu Event "Sommerfest 2026".
Submit erzeugt automatisch Gast mit RSVP=yes. Kein manuelles
Übertragen mehr nötig — direkter Pipeline-Vorteil gegenüber
Typeform/Tally.
svelte-check 0 errors. i18n-parity 6415 keys.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
e8774fc233
commit
57b7a43147
9 changed files with 285 additions and 34 deletions
|
|
@ -79,11 +79,21 @@
|
|||
"title": "Auto-Sync — bei Antwort erzeugen",
|
||||
"targetNone": "Nichts",
|
||||
"targetContacts": "Kontakt",
|
||||
"targetEvents": "Event-RSVP",
|
||||
"contactsHint": "Wähle für jedes Form-Feld, welches Kontakt-Feld es füllen soll. Leerlassen = ignorieren.",
|
||||
"eventsHint": "Wähle für jedes Form-Feld, welches Gast-Feld es füllen soll. RSVPs werden auf \"Zusage\" gesetzt.",
|
||||
"eventPicker": "Welches Event?",
|
||||
"eventPickerNone": "Bitte wählen ...",
|
||||
"eventNeeded": "Wähle das Event, zu dem die RSVPs angelegt werden sollen.",
|
||||
"needFields": "Lege mindestens ein Antwortfeld an, um Mapping zu konfigurieren.",
|
||||
"ignore": "Ignorieren",
|
||||
"contactKey": {
|
||||
"name": "Vor- + Nachname (auto split)"
|
||||
},
|
||||
"guestKey": {
|
||||
"name": "Name",
|
||||
"note": "Notiz",
|
||||
"plusOnes": "Begleitpersonen"
|
||||
}
|
||||
},
|
||||
"viewResponses": "Antworten ({n})"
|
||||
|
|
|
|||
|
|
@ -79,11 +79,21 @@
|
|||
"title": "Auto-sync — create on submit",
|
||||
"targetNone": "None",
|
||||
"targetContacts": "Contact",
|
||||
"targetEvents": "Event RSVP",
|
||||
"contactsHint": "For each form field, pick which contact field it should fill. Leave empty to ignore.",
|
||||
"eventsHint": "For each form field, pick which guest field it should fill. RSVPs are set to \"attending\".",
|
||||
"eventPicker": "Which event?",
|
||||
"eventPickerNone": "Please pick ...",
|
||||
"eventNeeded": "Pick the event the RSVPs should be created for.",
|
||||
"needFields": "Add at least one answer field to configure mapping.",
|
||||
"ignore": "Ignore",
|
||||
"contactKey": {
|
||||
"name": "First + last name (auto split)"
|
||||
},
|
||||
"guestKey": {
|
||||
"name": "Name",
|
||||
"note": "Note",
|
||||
"plusOnes": "Plus-ones"
|
||||
}
|
||||
},
|
||||
"viewResponses": "Responses ({n})"
|
||||
|
|
|
|||
|
|
@ -79,11 +79,21 @@
|
|||
"title": "Auto-sync — crear al recibir respuesta",
|
||||
"targetNone": "Nada",
|
||||
"targetContacts": "Contacto",
|
||||
"targetEvents": "RSVP de evento",
|
||||
"contactsHint": "Para cada campo del formulario, elige qué campo del contacto debe rellenar. Deja vacío para ignorar.",
|
||||
"eventsHint": "Para cada campo del formulario, elige qué campo del invitado debe rellenar. Los RSVPs se ponen en \"asistiré\".",
|
||||
"eventPicker": "¿Qué evento?",
|
||||
"eventPickerNone": "Selecciona ...",
|
||||
"eventNeeded": "Elige el evento al que se van a crear los RSVPs.",
|
||||
"needFields": "Añade al menos un campo de respuesta para configurar el mapeo.",
|
||||
"ignore": "Ignorar",
|
||||
"contactKey": {
|
||||
"name": "Nombre + apellido (auto split)"
|
||||
},
|
||||
"guestKey": {
|
||||
"name": "Nombre",
|
||||
"note": "Nota",
|
||||
"plusOnes": "Acompañantes"
|
||||
}
|
||||
},
|
||||
"viewResponses": "Respuestas ({n})"
|
||||
|
|
|
|||
|
|
@ -79,11 +79,21 @@
|
|||
"title": "Auto-sync — créer à la soumission",
|
||||
"targetNone": "Rien",
|
||||
"targetContacts": "Contact",
|
||||
"targetEvents": "RSVP événement",
|
||||
"contactsHint": "Pour chaque champ du formulaire, choisis quel champ de contact remplir. Laisse vide pour ignorer.",
|
||||
"eventsHint": "Pour chaque champ du formulaire, choisis quel champ d'invité remplir. Les RSVPs sont mis sur \"présent\".",
|
||||
"eventPicker": "Quel événement ?",
|
||||
"eventPickerNone": "Choisir ...",
|
||||
"eventNeeded": "Choisis l'événement pour lequel les RSVPs seront créés.",
|
||||
"needFields": "Ajoute au moins un champ de réponse pour configurer le mapping.",
|
||||
"ignore": "Ignorer",
|
||||
"contactKey": {
|
||||
"name": "Prénom + nom (auto split)"
|
||||
},
|
||||
"guestKey": {
|
||||
"name": "Nom",
|
||||
"note": "Note",
|
||||
"plusOnes": "Accompagnants"
|
||||
}
|
||||
},
|
||||
"viewResponses": "Réponses ({n})"
|
||||
|
|
|
|||
|
|
@ -79,11 +79,21 @@
|
|||
"title": "Auto-sync — crea al ricevere risposta",
|
||||
"targetNone": "Nessuno",
|
||||
"targetContacts": "Contatto",
|
||||
"targetEvents": "RSVP evento",
|
||||
"contactsHint": "Per ogni campo del modulo, scegli quale campo del contatto deve riempire. Lascia vuoto per ignorare.",
|
||||
"eventsHint": "Per ogni campo del modulo, scegli quale campo dell'ospite deve riempire. Gli RSVP vengono impostati su \"presente\".",
|
||||
"eventPicker": "Quale evento?",
|
||||
"eventPickerNone": "Seleziona ...",
|
||||
"eventNeeded": "Scegli l'evento per cui creare gli RSVP.",
|
||||
"needFields": "Aggiungi almeno un campo di risposta per configurare il mapping.",
|
||||
"ignore": "Ignora",
|
||||
"contactKey": {
|
||||
"name": "Nome + cognome (auto split)"
|
||||
},
|
||||
"guestKey": {
|
||||
"name": "Nome",
|
||||
"note": "Nota",
|
||||
"plusOnes": "Accompagnatori"
|
||||
}
|
||||
},
|
||||
"viewResponses": "Risposte ({n})"
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
-->
|
||||
<script lang="ts">
|
||||
import { _ } from 'svelte-i18n';
|
||||
import { useAllEvents } from '$lib/modules/events/queries';
|
||||
import type { AutoSyncTarget, FormField, FormSettings } from '../types';
|
||||
|
||||
let {
|
||||
|
|
@ -17,10 +18,13 @@
|
|||
onchange: (patch: Partial<FormSettings>) => void;
|
||||
} = $props();
|
||||
|
||||
// v1: nur 'contacts' implementiert (M7a). Andere Targets sind
|
||||
// strukturell vorgesehen, aber die dispatchTarget-Branch wirft —
|
||||
// wir filtern sie aus dem UI raus, bis M7b sie scharfschaltet.
|
||||
const SUPPORTED_TARGETS: AutoSyncTarget[] = ['contacts'];
|
||||
// M7a: contacts. M7b: events (RSVP). Andere Targets (feedback,
|
||||
// library, space_member) bleiben strukturell — dispatchTarget wirft,
|
||||
// UI filtert sie aus, bis sie scharfgeschaltet werden.
|
||||
const SUPPORTED_TARGETS: AutoSyncTarget[] = ['contacts', 'events'];
|
||||
|
||||
const events$ = useAllEvents();
|
||||
const events = $derived(events$.value);
|
||||
|
||||
const CONTACT_KEYS = [
|
||||
'name',
|
||||
|
|
@ -78,35 +82,74 @@
|
|||
}
|
||||
}
|
||||
|
||||
const GUEST_KEYS = ['name', 'email', 'phone', 'note', 'plusOnes'] as const;
|
||||
type GuestKey = (typeof GUEST_KEYS)[number];
|
||||
|
||||
function guestKeyLabel(key: GuestKey): string {
|
||||
switch (key) {
|
||||
case 'name':
|
||||
return $_('forms.builder.autoSync.guestKey.name', { default: 'Name' });
|
||||
case 'email':
|
||||
return 'E-Mail';
|
||||
case 'phone':
|
||||
return 'Telefon';
|
||||
case 'note':
|
||||
return $_('forms.builder.autoSync.guestKey.note', { default: 'Notiz' });
|
||||
case 'plusOnes':
|
||||
return $_('forms.builder.autoSync.guestKey.plusOnes', { default: 'Begleitpersonen' });
|
||||
}
|
||||
}
|
||||
|
||||
const ANSWER_FIELDS = $derived(
|
||||
fields.filter((f) => f.type !== 'section' && f.type !== 'consent')
|
||||
);
|
||||
|
||||
const target = $derived<AutoSyncTarget | 'none'>(settings.autoSync?.target ?? 'none');
|
||||
const targetId = $derived(settings.autoSync?.targetId ?? '');
|
||||
const mapping = $derived(settings.autoSync?.mapping ?? {});
|
||||
|
||||
function setTarget(next: AutoSyncTarget | 'none') {
|
||||
if (next === 'none') {
|
||||
onchange({ autoSync: undefined });
|
||||
} else {
|
||||
// Switching target → drop mapping (different target, different
|
||||
// allowed keys). targetId is only meaningful for events.
|
||||
onchange({
|
||||
autoSync: {
|
||||
target: next,
|
||||
mapping: settings.autoSync?.mapping ?? {},
|
||||
mapping: {},
|
||||
...(next === 'events' && targetId ? { targetId } : {}),
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function setMappingFor(fieldId: string, contactKey: ContactKey | '') {
|
||||
function setTargetId(eventId: string) {
|
||||
onchange({
|
||||
autoSync: {
|
||||
target: 'events',
|
||||
targetId: eventId || undefined,
|
||||
mapping,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function setMappingFor(fieldId: string, key: string) {
|
||||
const next = { ...mapping };
|
||||
if (!contactKey) {
|
||||
if (!key) {
|
||||
delete next[fieldId];
|
||||
} else {
|
||||
next[fieldId] = contactKey;
|
||||
next[fieldId] = key;
|
||||
}
|
||||
const t = settings.autoSync?.target ?? 'contacts';
|
||||
onchange({ autoSync: { target: t, mapping: next } });
|
||||
const tid = settings.autoSync?.targetId;
|
||||
onchange({
|
||||
autoSync: {
|
||||
target: t,
|
||||
mapping: next,
|
||||
...(tid ? { targetId: tid } : {}),
|
||||
},
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
|
|
@ -205,12 +248,40 @@
|
|||
<option value={t}>
|
||||
{t === 'contacts'
|
||||
? $_('forms.builder.autoSync.targetContacts', { default: 'Kontakt' })
|
||||
: t === 'events'
|
||||
? $_('forms.builder.autoSync.targetEvents', { default: 'Event-RSVP' })
|
||||
: t}
|
||||
</option>
|
||||
{/each}
|
||||
</select>
|
||||
|
||||
{#if target === 'contacts'}
|
||||
{#if target === 'events'}
|
||||
<label class="setting-row">
|
||||
<span class="setting-label">
|
||||
{$_('forms.builder.autoSync.eventPicker', { default: 'Welches Event?' })}
|
||||
</span>
|
||||
<select
|
||||
value={targetId}
|
||||
onchange={(e) => setTargetId((e.currentTarget as HTMLSelectElement).value)}
|
||||
>
|
||||
<option value=""
|
||||
>{$_('forms.builder.autoSync.eventPickerNone', { default: 'Bitte wählen ...' })}</option
|
||||
>
|
||||
{#each events as ev (ev.id)}
|
||||
<option value={ev.id}>{ev.title}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</label>
|
||||
{#if !targetId}
|
||||
<p class="hint">
|
||||
{$_('forms.builder.autoSync.eventNeeded', {
|
||||
default: 'Wähle das Event, zu dem die RSVPs angelegt werden sollen.',
|
||||
})}
|
||||
</p>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
{#if target !== 'none' && (target !== 'events' || targetId)}
|
||||
{#if ANSWER_FIELDS.length === 0}
|
||||
<p class="hint">
|
||||
{$_('forms.builder.autoSync.needFields', {
|
||||
|
|
@ -219,7 +290,12 @@
|
|||
</p>
|
||||
{:else}
|
||||
<p class="hint">
|
||||
{$_('forms.builder.autoSync.contactsHint', {
|
||||
{target === 'events'
|
||||
? $_('forms.builder.autoSync.eventsHint', {
|
||||
default:
|
||||
'Wähle für jedes Form-Feld, welches Gast-Feld es füllen soll. RSVPs werden auf "Zusage" gesetzt.',
|
||||
})
|
||||
: $_('forms.builder.autoSync.contactsHint', {
|
||||
default:
|
||||
'Wähle für jedes Form-Feld, welches Kontakt-Feld es füllen soll. Leerlassen = ignorieren.',
|
||||
})}
|
||||
|
|
@ -231,18 +307,20 @@
|
|||
<select
|
||||
class="contact-key-select"
|
||||
value={mapping[f.id] ?? ''}
|
||||
onchange={(e) =>
|
||||
setMappingFor(
|
||||
f.id,
|
||||
(e.currentTarget as HTMLSelectElement).value as ContactKey | ''
|
||||
)}
|
||||
onchange={(e) => setMappingFor(f.id, (e.currentTarget as HTMLSelectElement).value)}
|
||||
>
|
||||
<option value="">
|
||||
{$_('forms.builder.autoSync.ignore', { default: 'Ignorieren' })}
|
||||
</option>
|
||||
{#if target === 'events'}
|
||||
{#each GUEST_KEYS as gk}
|
||||
<option value={gk}>{guestKeyLabel(gk)}</option>
|
||||
{/each}
|
||||
{:else}
|
||||
{#each CONTACT_KEYS as ck}
|
||||
<option value={ck}>{contactKeyLabel(ck)}</option>
|
||||
{/each}
|
||||
{/if}
|
||||
</select>
|
||||
</div>
|
||||
{/each}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { describe, it, expect } from 'vitest';
|
||||
import { buildContactFromAnswers } from './auto-sync';
|
||||
import { buildContactFromAnswers, buildEventGuestFromAnswers } from './auto-sync';
|
||||
|
||||
describe('buildContactFromAnswers', () => {
|
||||
it('maps form-fields to contact-fields directly', () => {
|
||||
|
|
@ -51,3 +51,53 @@ describe('buildContactFromAnswers', () => {
|
|||
expect(result).toEqual({ firstName: 'Anna' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('buildEventGuestFromAnswers', () => {
|
||||
it('maps name/email/phone/note straight through', () => {
|
||||
const result = buildEventGuestFromAnswers(
|
||||
{
|
||||
'f-name': 'Anna Mustermann',
|
||||
'f-email': 'anna@example.com',
|
||||
'f-phone': '+49 30 12345',
|
||||
'f-note': 'Bringe Salat mit',
|
||||
},
|
||||
{ 'f-name': 'name', 'f-email': 'email', 'f-phone': 'phone', 'f-note': 'note' }
|
||||
);
|
||||
expect(result).toEqual({
|
||||
name: 'Anna Mustermann',
|
||||
email: 'anna@example.com',
|
||||
phone: '+49 30 12345',
|
||||
note: 'Bringe Salat mit',
|
||||
});
|
||||
});
|
||||
|
||||
it('parses plusOnes as a non-negative integer', () => {
|
||||
expect(buildEventGuestFromAnswers({ 'f-plus': '2' }, { 'f-plus': 'plusOnes' })).toEqual({
|
||||
plusOnes: 2,
|
||||
});
|
||||
expect(
|
||||
buildEventGuestFromAnswers({ 'f-plus': '2.7' as unknown as string }, { 'f-plus': 'plusOnes' })
|
||||
).toEqual({ plusOnes: 2 });
|
||||
// Negative + non-numeric → drop silently
|
||||
expect(buildEventGuestFromAnswers({ 'f-plus': '-1' }, { 'f-plus': 'plusOnes' })).toEqual({});
|
||||
expect(buildEventGuestFromAnswers({ 'f-plus': 'abc' }, { 'f-plus': 'plusOnes' })).toEqual({});
|
||||
});
|
||||
|
||||
it('skips empty / null / undefined answers', () => {
|
||||
const result = buildEventGuestFromAnswers(
|
||||
{ 'f-name': '', 'f-email': null },
|
||||
{ 'f-name': 'name', 'f-email': 'email' }
|
||||
);
|
||||
expect(result).toEqual({});
|
||||
});
|
||||
|
||||
it('ignores unknown contact-style keys (firstName/lastName)', () => {
|
||||
// guest model has no firstName — only `name`. Mappings to non-
|
||||
// guest keys are silently dropped.
|
||||
const result = buildEventGuestFromAnswers(
|
||||
{ 'f-fn': 'Anna', 'f-name': 'Anna Mustermann' },
|
||||
{ 'f-fn': 'firstName', 'f-name': 'name' }
|
||||
);
|
||||
expect(result).toEqual({ name: 'Anna Mustermann' });
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -22,10 +22,11 @@
|
|||
*/
|
||||
|
||||
import { contactsStore } from '$lib/modules/contacts/stores/contacts.svelte';
|
||||
import { eventGuestsStore } from '$lib/modules/events/stores/guests.svelte';
|
||||
import { decryptRecords, isVaultUnlocked } from '$lib/data/crypto';
|
||||
import { formResponseTable, formTable } from '../collections';
|
||||
import { toForm, toFormResponse } from '../queries';
|
||||
import type { AnswerValue, AutoSyncTarget, Form, FormResponse } from '../types';
|
||||
import type { AnswerValue, AutoSyncConfig, Form, FormResponse } from '../types';
|
||||
|
||||
/**
|
||||
* Build a contact-record patch from a form response, given the
|
||||
|
|
@ -81,7 +82,7 @@ export async function applyAutoSync(
|
|||
const already = (response.syncedTargets ?? []).find((t) => t.target === cfg.target);
|
||||
if (already) return { synced: false, recordId: already.recordId };
|
||||
|
||||
const recordId = await dispatchTarget(cfg.target, cfg.mapping, response.answers);
|
||||
const recordId = await dispatchTarget(cfg, response.answers);
|
||||
if (!recordId) return { synced: false };
|
||||
|
||||
const next = [...(response.syncedTargets ?? []), { target: cfg.target, recordId }];
|
||||
|
|
@ -89,14 +90,62 @@ export async function applyAutoSync(
|
|||
return { synced: true, recordId };
|
||||
}
|
||||
|
||||
/**
|
||||
* Build an event-guest patch from a form response. Pure. The `name`
|
||||
* key is the synthetic auto-split target (firstName + lastName joined
|
||||
* with whitespace) — guest records carry a single `name` field, so we
|
||||
* collapse the contact-style split here.
|
||||
*/
|
||||
export function buildEventGuestFromAnswers(
|
||||
answers: Record<string, AnswerValue>,
|
||||
mapping: Record<string, string>
|
||||
): {
|
||||
name?: string;
|
||||
email?: string;
|
||||
phone?: string;
|
||||
note?: string;
|
||||
plusOnes?: number;
|
||||
} {
|
||||
const guest: { name?: string; email?: string; phone?: string; note?: string; plusOnes?: number } =
|
||||
{};
|
||||
|
||||
for (const [fieldId, key] of Object.entries(mapping)) {
|
||||
const value = answers[fieldId];
|
||||
if (value === null || value === undefined) continue;
|
||||
const str = typeof value === 'string' ? value.trim() : String(value);
|
||||
if (!str) continue;
|
||||
|
||||
switch (key) {
|
||||
case 'name':
|
||||
guest.name = str;
|
||||
break;
|
||||
case 'email':
|
||||
guest.email = str;
|
||||
break;
|
||||
case 'phone':
|
||||
guest.phone = str;
|
||||
break;
|
||||
case 'note':
|
||||
guest.note = str;
|
||||
break;
|
||||
case 'plusOnes': {
|
||||
const n = Number(str);
|
||||
if (Number.isFinite(n) && n >= 0) guest.plusOnes = Math.floor(n);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return guest;
|
||||
}
|
||||
|
||||
async function dispatchTarget(
|
||||
target: AutoSyncTarget,
|
||||
mapping: Record<string, string>,
|
||||
cfg: AutoSyncConfig,
|
||||
answers: Record<string, AnswerValue>
|
||||
): Promise<string | null> {
|
||||
switch (target) {
|
||||
switch (cfg.target) {
|
||||
case 'contacts': {
|
||||
const data = buildContactFromAnswers(answers, mapping);
|
||||
const data = buildContactFromAnswers(answers, cfg.mapping);
|
||||
// Need at least a name or email to create a contact — anything
|
||||
// less leaks empty rows into /contacts.
|
||||
if (!data.firstName && !data.lastName && !data.email) {
|
||||
|
|
@ -105,13 +154,35 @@ async function dispatchTarget(
|
|||
const contact = await contactsStore.createContact(data);
|
||||
return contact?.id ?? null;
|
||||
}
|
||||
case 'events':
|
||||
case 'events': {
|
||||
if (!cfg.targetId) {
|
||||
throw new Error('autoSync.targetId (eventId) ist erforderlich für target=events');
|
||||
}
|
||||
const guest = buildEventGuestFromAnswers(answers, cfg.mapping);
|
||||
// Need at least a name to create a guest entry. Without one
|
||||
// the RSVP list collects empty rows.
|
||||
if (!guest.name) return null;
|
||||
const result = await eventGuestsStore.addGuest({
|
||||
eventId: cfg.targetId,
|
||||
name: guest.name,
|
||||
email: guest.email ?? null,
|
||||
phone: guest.phone ?? null,
|
||||
note: guest.note ?? null,
|
||||
plusOnes: guest.plusOnes ?? 0,
|
||||
rsvpStatus: 'yes',
|
||||
});
|
||||
return result.success ? result.id : null;
|
||||
}
|
||||
case 'feedback':
|
||||
case 'library':
|
||||
case 'space_member':
|
||||
// Future M7b — surfaces left wired so the planner doesn't
|
||||
// silently no-op when the user picks an unsupported target.
|
||||
throw new Error(`autoSync target "${target}" is not yet implemented (M7b)`);
|
||||
// Out of M7b scope. feedback ist zentraler Public-Hub (eigene
|
||||
// Domain, keine Dexie), library + space_member brauchen mehr
|
||||
// Architektur (target-id picker für library, invite-flow für
|
||||
// space_member). Bleibt explicit "not yet" damit der UI-Filter
|
||||
// im SettingsPanel keine option-Werte vergibt, die runtime
|
||||
// brechen wuerden.
|
||||
throw new Error(`autoSync target "${cfg.target}" ist noch nicht implementiert`);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -72,6 +72,8 @@ export type AutoSyncTarget = 'contacts' | 'events' | 'feedback' | 'library' | 's
|
|||
|
||||
export interface AutoSyncConfig {
|
||||
target: AutoSyncTarget;
|
||||
/** Optional anchor — for `events` this is the eventId the response RSVPs to. */
|
||||
targetId?: string;
|
||||
mapping: Record<string, string>;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue