@acms-subir/core 0.3.39 → 0.3.41
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +19 -0
- package/cli/migrations.generated.mjs +5 -0
- package/docs/mobile.md +2 -1
- package/integration.mjs +3 -0
- package/mobile-template/recipes/events/files/components/RsvpPanel.tsx +66 -31
- package/mobile-template/recipes/rsvp/files/components/RsvpPanel.tsx +66 -31
- package/mobile-template/recipes/rsvp/recipe.json +2 -2
- package/package.json +1 -1
- package/src/actions/index.ts +1 -1
- package/src/components/EventPage.astro +16 -8
- package/src/components/editor/EventPanel.tsx +3 -2
- package/src/components/editor/PostEditor.tsx +1 -1
- package/src/components/editor/event/RegistrationCard.tsx +9 -0
- package/src/components/editor/event/registration.ts +1 -1
- package/src/lib/booking/agent.ts +1 -1
- package/src/lib/booking/emails.ts +54 -15
- package/src/lib/booking/service.ts +207 -23
- package/src/lib/calendar/api.ts +3 -2
- package/src/lib/calendar/members-config.ts +5 -0
- package/src/lib/calendar/requests.ts +4 -4
- package/src/lib/calendar/rsvp-words.ts +69 -8
- package/src/lib/content-service.ts +13 -5
- package/src/lib/drafts.ts +2 -2
- package/src/lib/editor/bookings.ts +5 -3
- package/src/lib/editor/documents.ts +4 -2
- package/src/lib/editor/schemas.ts +1 -1
- package/src/lib/email/booking-words.ts +82 -2
- package/src/lib/email/service.ts +1 -1
- package/src/lib/email/store.ts +17 -1
- package/src/lib/email/types.ts +3 -1
- package/src/lib/forms/events.ts +4 -2
- package/src/lib/legal/app-privacy.ts +2 -2
- package/src/lib/legal/text-en.ts +1 -1
- package/src/lib/legal/text-es.ts +1 -1
- package/src/lib/mobile/bookings.ts +4 -3
- package/src/lib/mobile/calendar.ts +23 -1
- package/src/lib/mobile/manage.ts +18 -4
- package/src/lib/mobile/openapi.ts +6 -3
- package/src/lib/mobile/rsvp.ts +10 -5
- package/src/lib/mobile/schemas.ts +12 -4
- package/src/lib/repo/migrations.ts +5 -0
- package/src/lib/repo/sql.ts +54 -13
- package/src/lib/repo/types.ts +15 -5
- package/src/lib/scheduler.ts +3 -1
- package/src/lib/schema.ts +13 -2
- package/src/pages/admin/calendar/bookings.astro +57 -13
- package/src/pages/api/calendar/bookings/index.ts +1 -1
- package/src/pages/api/calendar/docs.ts +2 -2
- package/src/pages/api/v1/bookings/manage/confirm.ts +6 -0
- package/src/pages/api/v1/events/[slug]/index.ts +8 -0
- package/src/pages/book/manage/[token].astro +57 -15
- package/src/pages/events/[slug].astro +3 -2
- package/src/styles/admin.css +3 -1
- package/src/styles/site.css +10 -1
- package/src/theme-sdk/index.ts +1 -1
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
// Words of the built-in RSVP: the event page's RSVP block, the RSVP's own
|
|
2
2
|
// page (book/manage) and the app's default button (GET /events/:slug/rsvp).
|
|
3
3
|
// The same wording as the app's RSVP panel (packages/native strings.ts).
|
|
4
|
-
// Plain strings with {count}, {email}, {title}, {date}
|
|
5
|
-
// (live/words.ts `fill`); of a pair
|
|
4
|
+
// Plain strings with {count}, {email}, {title}, {date}, {time}, {position},
|
|
5
|
+
// {seats} and {hours} slots (live/words.ts `fill`); of a pair
|
|
6
|
+
// (`spotLeft`/`spotsLeft`), the first is for 1 and the
|
|
6
7
|
// second for any other number. The event's own form title, questions and
|
|
7
8
|
// button keep their words. Other languages fall back to English.
|
|
8
9
|
export interface RsvpStrings {
|
|
@@ -28,6 +29,31 @@ export interface RsvpStrings {
|
|
|
28
29
|
duplicate: string;
|
|
29
30
|
duplicateNote: string;
|
|
30
31
|
saveError: string;
|
|
32
|
+
/** a full date with a waitlist: "Full", "40 going · no spots left", the note and the button (also the form's) */
|
|
33
|
+
fullTag: string;
|
|
34
|
+
noSpotsLeft: string;
|
|
35
|
+
waitlistNote: string;
|
|
36
|
+
joinWaitlist: string;
|
|
37
|
+
/** the RSVP's own page for a place in line: "#3 in line · 2 seats" ({seats}: `seat`/`seats`), and the promise ({hours}) */
|
|
38
|
+
onWaitlist: string;
|
|
39
|
+
inLine: string;
|
|
40
|
+
seat: string;
|
|
41
|
+
seats: string;
|
|
42
|
+
waitlistPromise: string;
|
|
43
|
+
leaveWaitlist: string;
|
|
44
|
+
leaveConfirm: string;
|
|
45
|
+
/** off the line: they left it, released the spot or staff removed them */
|
|
46
|
+
offWaitlist: string;
|
|
47
|
+
offWaitlistText: string;
|
|
48
|
+
/** a spot held for them ({date} and {time}: until when), and one they didn't confirm in time */
|
|
49
|
+
spotOpened: string;
|
|
50
|
+
spotYours: string;
|
|
51
|
+
confirmSpot: string;
|
|
52
|
+
releaseSpot: string;
|
|
53
|
+
releaseConfirm: string;
|
|
54
|
+
released: string;
|
|
55
|
+
releasedText: string;
|
|
56
|
+
confirmError: string;
|
|
31
57
|
/** the RSVP's own page */
|
|
32
58
|
booked: string;
|
|
33
59
|
yours: string;
|
|
@@ -44,7 +70,7 @@ export interface RsvpStrings {
|
|
|
44
70
|
where: string;
|
|
45
71
|
status: string;
|
|
46
72
|
reference: string;
|
|
47
|
-
statuses: Record<'pending' | 'confirmed' | 'cancelled' | 'declined', string>;
|
|
73
|
+
statuses: Record<'pending' | 'confirmed' | 'cancelled' | 'declined' | 'waitlisted' | 'offered' | 'expired', string>;
|
|
48
74
|
addToCalendar: string;
|
|
49
75
|
downloadIcs: string;
|
|
50
76
|
cancel: string;
|
|
@@ -61,11 +87,18 @@ const STRINGS: Record<string, RsvpStrings> = {
|
|
|
61
87
|
full: 'This event is full.', closed: 'RSVPs are closed.', saveError: 'Couldn’t save your RSVP. Please try again.',
|
|
62
88
|
closesOn: 'RSVPs close {date} at {time}', closedOn: 'They closed on {date}. Questions? Contact us.',
|
|
63
89
|
duplicate: 'You already have an RSVP for this date.', duplicateNote: 'We sent you an email with a link to change it. Need more seats? Change it there.',
|
|
90
|
+
fullTag: 'Full', noSpotsLeft: 'no spots left', waitlistNote: 'Join the waitlist and we’ll email you if a spot opens.', joinWaitlist: 'Join the waitlist',
|
|
91
|
+
onWaitlist: 'You’re on the waitlist', inLine: '#{position} in line · {seats}', seat: '{count} seat', seats: '{count} seats',
|
|
92
|
+
waitlistPromise: 'If a spot opens, we’ll email you and save it for {hours} hours.', leaveWaitlist: 'Leave the waitlist', leaveConfirm: 'Leave the waitlist?',
|
|
93
|
+
offWaitlist: 'You’re off the waitlist', offWaitlistText: 'You’re no longer on the waitlist for this date.',
|
|
94
|
+
spotOpened: 'A spot opened', spotYours: 'A spot is yours until {date} at {time}', confirmSpot: 'Confirm my spot', releaseSpot: 'Release it',
|
|
95
|
+
releaseConfirm: 'Release this spot? It goes to the next person in line.', released: 'This held spot was released',
|
|
96
|
+
releasedText: 'We held it for {hours} hours, then offered it to the next person in line.', confirmError: 'Couldn’t confirm — please try again.',
|
|
64
97
|
booked: 'You’re booked', yours: 'Your booking', requestSent: 'Request sent', awaitingApproval: 'Waiting for approval', notAvailable: 'Not available', cancelledTitle: 'Booking cancelled',
|
|
65
98
|
emailWhenApproved: 'We’ll email {email} once it’s approved.', confirmationOnItsWay: 'A confirmation is on its way to {email}.',
|
|
66
99
|
declined: 'Sorry, the organiser couldn’t confirm it.', cancelledText: 'This booking is cancelled.',
|
|
67
100
|
what: 'What', when: 'When', where: 'Where', status: 'Status', reference: 'Reference',
|
|
68
|
-
statuses: { pending: 'Pending', confirmed: 'Confirmed', cancelled: 'Cancelled', declined: 'Declined' },
|
|
101
|
+
statuses: { pending: 'Pending', confirmed: 'Confirmed', cancelled: 'Cancelled', declined: 'Declined', waitlisted: 'On the waitlist', offered: 'Spot held for you', expired: 'Released' },
|
|
69
102
|
addToCalendar: 'Add to calendar', downloadIcs: 'Download .ics', cancel: 'Cancel RSVP', cancelConfirm: 'Cancel this RSVP?', cancelError: 'Couldn’t cancel — please try again.',
|
|
70
103
|
backTo: 'Back to {title}', keepLink: 'Keep this link — anyone with it can change this booking.',
|
|
71
104
|
},
|
|
@@ -75,11 +108,18 @@ const STRINGS: Record<string, RsvpStrings> = {
|
|
|
75
108
|
full: 'Este evento está lleno.', closed: 'Las confirmaciones están cerradas.', saveError: 'No se pudo guardar tu confirmación. Inténtalo de nuevo.',
|
|
76
109
|
closesOn: 'Las confirmaciones cierran el {date}, {time}', closedOn: 'Cerraron el {date}. ¿Preguntas? Contáctanos.',
|
|
77
110
|
duplicate: 'Ya tienes una confirmación para esta fecha.', duplicateNote: 'Te enviamos un correo con un enlace para cambiarla. ¿Necesitas más lugares? Cámbiala ahí.',
|
|
111
|
+
fullTag: 'Lleno', noSpotsLeft: 'no quedan lugares', waitlistNote: 'Únete a la lista de espera y te escribiremos si se libera un lugar.', joinWaitlist: 'Unirme a la lista de espera',
|
|
112
|
+
onWaitlist: 'Estás en la lista de espera', inLine: '#{position} en la fila · {seats}', seat: '{count} lugar', seats: '{count} lugares',
|
|
113
|
+
waitlistPromise: 'Si se libera un lugar, te escribiremos y te lo apartaremos {hours} horas.', leaveWaitlist: 'Salir de la lista de espera', leaveConfirm: '¿Salir de la lista de espera?',
|
|
114
|
+
offWaitlist: 'Ya no estás en la lista de espera', offWaitlistText: 'Ya no estás en la lista de espera para esta fecha.',
|
|
115
|
+
spotOpened: 'Se liberó un lugar', spotYours: 'El lugar es tuyo hasta el {date}, {time}', confirmSpot: 'Confirmar mi lugar', releaseSpot: 'Liberarlo',
|
|
116
|
+
releaseConfirm: '¿Liberar este lugar? Pasará a la siguiente persona en la fila.', released: 'Se liberó el lugar que te apartamos',
|
|
117
|
+
releasedText: 'Te lo apartamos {hours} horas y luego se lo ofrecimos a la siguiente persona en la fila.', confirmError: 'No se pudo confirmar. Inténtalo de nuevo.',
|
|
78
118
|
booked: 'Ya tienes tu lugar', yours: 'Tu confirmación', requestSent: 'Solicitud enviada', awaitingApproval: 'En espera de aprobación', notAvailable: 'No se pudo confirmar', cancelledTitle: 'Confirmación cancelada',
|
|
79
119
|
emailWhenApproved: 'Te escribiremos a {email} cuando se apruebe.', confirmationOnItsWay: 'Te enviamos la confirmación a {email}.',
|
|
80
120
|
declined: 'Lo sentimos, el organizador no pudo confirmarla.', cancelledText: 'Esta confirmación se canceló.',
|
|
81
121
|
what: 'Qué', when: 'Cuándo', where: 'Dónde', status: 'Estado', reference: 'Referencia',
|
|
82
|
-
statuses: { pending: 'Pendiente', confirmed: 'Confirmado', cancelled: 'Cancelado', declined: 'Rechazado' },
|
|
122
|
+
statuses: { pending: 'Pendiente', confirmed: 'Confirmado', cancelled: 'Cancelado', declined: 'Rechazado', waitlisted: 'En lista de espera', offered: 'Lugar apartado para ti', expired: 'Liberado' },
|
|
83
123
|
addToCalendar: 'Agregar al calendario', downloadIcs: 'Descargar .ics', cancel: 'Cancelar mi confirmación', cancelConfirm: '¿Cancelar tu confirmación?', cancelError: 'No se pudo cancelar. Inténtalo de nuevo.',
|
|
84
124
|
backTo: 'Volver a {title}', keepLink: 'Guarda este enlace: quien lo tenga puede cambiar esta confirmación.',
|
|
85
125
|
},
|
|
@@ -89,11 +129,18 @@ const STRINGS: Record<string, RsvpStrings> = {
|
|
|
89
129
|
full: 'Cet événement est complet.', closed: 'Les inscriptions sont closes.', saveError: 'Impossible d’enregistrer votre inscription. Veuillez réessayer.',
|
|
90
130
|
closesOn: 'Les inscriptions ferment le {date} à {time}', closedOn: 'Elles ont fermé le {date}. Des questions ? Contactez-nous.',
|
|
91
131
|
duplicate: 'Vous avez déjà une inscription pour cette date.', duplicateNote: 'Nous vous avons envoyé un e-mail avec un lien pour la modifier. Besoin de plus de places ? Modifiez-la depuis ce lien.',
|
|
132
|
+
fullTag: 'Complet', noSpotsLeft: 'plus de place', waitlistNote: 'Inscrivez-vous sur la liste d’attente : nous vous écrirons si une place se libère.', joinWaitlist: 'Rejoindre la liste d’attente',
|
|
133
|
+
onWaitlist: 'Vous êtes sur la liste d’attente', inLine: 'N° {position} dans la file · {seats}', seat: '{count} place', seats: '{count} places',
|
|
134
|
+
waitlistPromise: 'Si une place se libère, nous vous écrirons et vous la garderons {hours} heures.', leaveWaitlist: 'Quitter la liste d’attente', leaveConfirm: 'Quitter la liste d’attente ?',
|
|
135
|
+
offWaitlist: 'Vous n’êtes plus sur la liste d’attente', offWaitlistText: 'Vous n’êtes plus sur la liste d’attente pour cette date.',
|
|
136
|
+
spotOpened: 'Une place s’est libérée', spotYours: 'Une place est à vous jusqu’au {date} à {time}', confirmSpot: 'Confirmer ma place', releaseSpot: 'La libérer',
|
|
137
|
+
releaseConfirm: 'Libérer cette place ? Elle ira à la personne suivante dans la file.', released: 'La place gardée pour vous a été libérée',
|
|
138
|
+
releasedText: 'Nous l’avons gardée {hours} heures, puis proposée à la personne suivante dans la file.', confirmError: 'Impossible de confirmer. Veuillez réessayer.',
|
|
92
139
|
booked: 'Votre place est réservée', yours: 'Votre inscription', requestSent: 'Demande envoyée', awaitingApproval: 'En attente de validation', notAvailable: 'Non confirmée', cancelledTitle: 'Inscription annulée',
|
|
93
140
|
emailWhenApproved: 'Nous écrirons à {email} dès qu’elle sera validée.', confirmationOnItsWay: 'Une confirmation est en route vers {email}.',
|
|
94
141
|
declined: 'Désolés, l’organisateur n’a pas pu la confirmer.', cancelledText: 'Cette inscription est annulée.',
|
|
95
142
|
what: 'Quoi', when: 'Quand', where: 'Où', status: 'Statut', reference: 'Référence',
|
|
96
|
-
statuses: { pending: 'En attente', confirmed: 'Confirmée', cancelled: 'Annulée', declined: 'Refusée' },
|
|
143
|
+
statuses: { pending: 'En attente', confirmed: 'Confirmée', cancelled: 'Annulée', declined: 'Refusée', waitlisted: 'Sur liste d’attente', offered: 'Place gardée pour vous', expired: 'Libérée' },
|
|
97
144
|
addToCalendar: 'Ajouter au calendrier', downloadIcs: 'Télécharger le .ics', cancel: 'Annuler mon inscription', cancelConfirm: 'Annuler cette inscription ?', cancelError: 'Impossible d’annuler. Veuillez réessayer.',
|
|
98
145
|
backTo: 'Retour à {title}', keepLink: 'Conservez ce lien : toute personne qui l’a peut modifier cette inscription.',
|
|
99
146
|
},
|
|
@@ -103,11 +150,18 @@ const STRINGS: Record<string, RsvpStrings> = {
|
|
|
103
150
|
full: 'Diese Veranstaltung ist ausgebucht.', closed: 'Anmeldungen sind geschlossen.', saveError: 'Ihre Anmeldung konnte nicht gespeichert werden. Bitte versuchen Sie es erneut.',
|
|
104
151
|
closesOn: 'Anmeldung bis {date}, {time}', closedOn: 'Die Anmeldung endete am {date}. Fragen? Kontaktieren Sie uns.',
|
|
105
152
|
duplicate: 'Sie haben für diesen Termin schon eine Anmeldung.', duplicateNote: 'Wir haben Ihnen eine E-Mail mit einem Link zum Ändern geschickt. Mehr Plätze nötig? Ändern Sie sie dort.',
|
|
153
|
+
fullTag: 'Ausgebucht', noSpotsLeft: 'keine Plätze frei', waitlistNote: 'Tragen Sie sich auf die Warteliste ein. Wird ein Platz frei, schreiben wir Ihnen.', joinWaitlist: 'Auf die Warteliste',
|
|
154
|
+
onWaitlist: 'Sie stehen auf der Warteliste', inLine: 'Nr. {position} auf der Warteliste · {seats}', seat: '{count} Platz', seats: '{count} Plätze',
|
|
155
|
+
waitlistPromise: 'Wird ein Platz frei, schreiben wir Ihnen und halten ihn {hours} Stunden für Sie frei.', leaveWaitlist: 'Warteliste verlassen', leaveConfirm: 'Die Warteliste verlassen?',
|
|
156
|
+
offWaitlist: 'Sie stehen nicht mehr auf der Warteliste', offWaitlistText: 'Sie stehen für diesen Termin nicht mehr auf der Warteliste.',
|
|
157
|
+
spotOpened: 'Ein Platz ist frei geworden', spotYours: 'Der Platz ist bis {date}, {time} für Sie reserviert', confirmSpot: 'Platz bestätigen', releaseSpot: 'Freigeben',
|
|
158
|
+
releaseConfirm: 'Diesen Platz freigeben? Er geht an die nächste Person auf der Warteliste.', released: 'Der für Sie reservierte Platz wurde freigegeben',
|
|
159
|
+
releasedText: 'Wir haben ihn {hours} Stunden für Sie freigehalten und dann der nächsten Person auf der Warteliste angeboten.', confirmError: 'Bestätigung fehlgeschlagen. Bitte versuchen Sie es erneut.',
|
|
106
160
|
booked: 'Ihr Platz ist reserviert', yours: 'Ihre Anmeldung', requestSent: 'Anfrage gesendet', awaitingApproval: 'Wartet auf Freigabe', notAvailable: 'Nicht bestätigt', cancelledTitle: 'Anmeldung storniert',
|
|
107
161
|
emailWhenApproved: 'Wir schreiben an {email}, sobald sie freigegeben ist.', confirmationOnItsWay: 'Eine Bestätigung ist unterwegs an {email}.',
|
|
108
162
|
declined: 'Leider konnte der Veranstalter sie nicht bestätigen.', cancelledText: 'Diese Anmeldung ist storniert.',
|
|
109
163
|
what: 'Was', when: 'Wann', where: 'Wo', status: 'Status', reference: 'Referenz',
|
|
110
|
-
statuses: { pending: 'Ausstehend', confirmed: 'Bestätigt', cancelled: 'Storniert', declined: 'Abgelehnt' },
|
|
164
|
+
statuses: { pending: 'Ausstehend', confirmed: 'Bestätigt', cancelled: 'Storniert', declined: 'Abgelehnt', waitlisted: 'Auf der Warteliste', offered: 'Platz für Sie reserviert', expired: 'Freigegeben' },
|
|
111
165
|
addToCalendar: 'Zum Kalender hinzufügen', downloadIcs: '.ics herunterladen', cancel: 'Anmeldung stornieren', cancelConfirm: 'Diese Anmeldung stornieren?', cancelError: 'Stornieren fehlgeschlagen. Bitte versuchen Sie es erneut.',
|
|
112
166
|
backTo: 'Zurück zu {title}', keepLink: 'Bewahren Sie diesen Link auf – wer ihn hat, kann diese Anmeldung ändern.',
|
|
113
167
|
},
|
|
@@ -117,11 +171,18 @@ const STRINGS: Record<string, RsvpStrings> = {
|
|
|
117
171
|
full: 'Este evento está lotado.', closed: 'As inscrições estão encerradas.', saveError: 'Não foi possível salvar sua inscrição. Tente novamente.',
|
|
118
172
|
closesOn: 'As inscrições se encerram em {date}, {time}', closedOn: 'Elas se encerraram em {date}. Dúvidas? Fale com a gente.',
|
|
119
173
|
duplicate: 'Você já tem uma inscrição para esta data.', duplicateNote: 'Enviamos um e-mail com um link para alterá-la. Precisa de mais vagas? Altere por lá.',
|
|
174
|
+
fullTag: 'Lotado', noSpotsLeft: 'nenhuma vaga', waitlistNote: 'Entre na lista de espera e avisaremos por e-mail se abrir uma vaga.', joinWaitlist: 'Entrar na lista de espera',
|
|
175
|
+
onWaitlist: 'Você está na lista de espera', inLine: 'Nº {position} na fila · {seats}', seat: '{count} vaga', seats: '{count} vagas',
|
|
176
|
+
waitlistPromise: 'Se abrir uma vaga, avisaremos por e-mail e vamos guardá-la por {hours} horas.', leaveWaitlist: 'Sair da lista de espera', leaveConfirm: 'Sair da lista de espera?',
|
|
177
|
+
offWaitlist: 'Você não está mais na lista de espera', offWaitlistText: 'Você não está mais na lista de espera desta data.',
|
|
178
|
+
spotOpened: 'Abriu uma vaga', spotYours: 'A vaga é sua até {date}, {time}', confirmSpot: 'Confirmar minha vaga', releaseSpot: 'Liberar',
|
|
179
|
+
releaseConfirm: 'Liberar esta vaga? Ela passa para a próxima pessoa da fila.', released: 'A vaga guardada para você foi liberada',
|
|
180
|
+
releasedText: 'Nós a guardamos por {hours} horas e depois a oferecemos à próxima pessoa da fila.', confirmError: 'Não foi possível confirmar. Tente novamente.',
|
|
120
181
|
booked: 'Sua vaga está reservada', yours: 'Sua inscrição', requestSent: 'Solicitação enviada', awaitingApproval: 'Aguardando aprovação', notAvailable: 'Não confirmada', cancelledTitle: 'Inscrição cancelada',
|
|
121
182
|
emailWhenApproved: 'Vamos escrever para {email} assim que for aprovada.', confirmationOnItsWay: 'Uma confirmação está a caminho de {email}.',
|
|
122
183
|
declined: 'Desculpe, o organizador não pôde confirmá-la.', cancelledText: 'Esta inscrição foi cancelada.',
|
|
123
184
|
what: 'O quê', when: 'Quando', where: 'Onde', status: 'Status', reference: 'Referência',
|
|
124
|
-
statuses: { pending: 'Pendente', confirmed: 'Confirmada', cancelled: 'Cancelada', declined: 'Recusada' },
|
|
185
|
+
statuses: { pending: 'Pendente', confirmed: 'Confirmada', cancelled: 'Cancelada', declined: 'Recusada', waitlisted: 'Na lista de espera', offered: 'Vaga guardada para você', expired: 'Liberada' },
|
|
125
186
|
addToCalendar: 'Adicionar ao calendário', downloadIcs: 'Baixar .ics', cancel: 'Cancelar inscrição', cancelConfirm: 'Cancelar esta inscrição?', cancelError: 'Não foi possível cancelar. Tente novamente.',
|
|
126
187
|
backTo: 'Voltar para {title}', keepLink: 'Guarde este link: qualquer pessoa com ele pode alterar esta inscrição.',
|
|
127
188
|
},
|
|
@@ -8,6 +8,7 @@ import { CONTENT, collectionUrl } from './content';
|
|
|
8
8
|
import { CALENDAR } from './calendar/config';
|
|
9
9
|
import { formatWall, isValidTimeZone, normalizeEventTimes, parseWall } from './calendar/events';
|
|
10
10
|
import { eventChanged } from './calendar/hooks';
|
|
11
|
+
import { offerEventWaitlist } from './booking/service';
|
|
11
12
|
import { normalizePageLocale } from './i18n';
|
|
12
13
|
import { siteOrigin } from './seo';
|
|
13
14
|
import { waitUntilFrom } from './webhooks';
|
|
@@ -37,10 +38,10 @@ export type PageSave = Omit<Parameters<ContentRepository['updatePage']>[1], 'blo
|
|
|
37
38
|
// location (see mergeLocationInput) — older clients send name/address/url only
|
|
38
39
|
// cancelledDates is left out: single dates are cancelled at once through
|
|
39
40
|
// cancelOccurrence/restoreOccurrence (lib/calendar/schedule.ts), never staged
|
|
40
|
-
// rsvp may leave out the deadline (older clients): the current
|
|
41
|
+
// rsvp may leave out the deadline, notifyStaff and waitlist (older clients): the current ones stay
|
|
41
42
|
export type EventSave = Partial<Omit<CalEvent, 'id' | 'draft' | 'startAt' | 'endAt' | 'createdAt' | 'updatedAt' | 'sourceUid' | 'publishedAt' | 'location' | 'cancelledDates' | 'rsvp'>> & {
|
|
42
43
|
location?: Partial<EventLocation>;
|
|
43
|
-
rsvp?: Omit<CalEvent['rsvp'], 'closesAt' | 'closeBefore'> & Partial<Pick<CalEvent['rsvp'], 'closesAt' | 'closeBefore'>>;
|
|
44
|
+
rsvp?: Omit<CalEvent['rsvp'], 'closesAt' | 'closeBefore' | 'notifyStaff' | 'waitlist'> & Partial<Pick<CalEvent['rsvp'], 'closesAt' | 'closeBefore' | 'notifyStaff' | 'waitlist'>>;
|
|
44
45
|
};
|
|
45
46
|
type EventClean = Omit<EventSave, 'location' | 'rsvp'> & { location?: EventLocation; rsvp?: CalEvent['rsvp'] };
|
|
46
47
|
|
|
@@ -136,9 +137,11 @@ export async function cleanEventInput(repo: ContentRepository, data: EventSave,
|
|
|
136
137
|
const out: EventClean = { ...rest };
|
|
137
138
|
if (rsvp) {
|
|
138
139
|
// Older editor clients resend the original RSVP shape. Omission must not
|
|
139
|
-
// erase questions
|
|
140
|
+
// erase questions, a deadline, the staff emails setting or the waitlist
|
|
141
|
+
// that were authored in a newer editor.
|
|
140
142
|
const previous = mergeEventDraft(before).rsvp;
|
|
141
|
-
const next: CalEvent['rsvp'] = { ...rsvp, closesAt: rsvp.closesAt === undefined ? previous.closesAt ?? null : rsvp.closesAt || null, closeBefore: rsvp.closeBefore ?? previous.closeBefore ?? 0
|
|
143
|
+
const next: CalEvent['rsvp'] = { ...rsvp, closesAt: rsvp.closesAt === undefined ? previous.closesAt ?? null : rsvp.closesAt || null, closeBefore: rsvp.closeBefore ?? previous.closeBefore ?? 0,
|
|
144
|
+
notifyStaff: rsvp.notifyStaff ?? previous.notifyStaff ?? true, waitlist: rsvp.waitlist ?? previous.waitlist ?? false };
|
|
142
145
|
if (!Object.hasOwn(rsvp, 'form') && previous.form) next.form = previous.form;
|
|
143
146
|
if (next.form !== undefined) {
|
|
144
147
|
const form = FormDefinition.safeParse(next.form);
|
|
@@ -153,6 +156,8 @@ export async function cleanEventInput(repo: ContentRepository, data: EventSave,
|
|
|
153
156
|
if (!Number.isInteger(next.closeBefore) || next.closeBefore < 0 || next.closeBefore > 60 * 1440) {
|
|
154
157
|
throw new ApiError(400, 'The RSVP deadline can be at most 60 days before each date (closeBefore: 0–86400 minutes).');
|
|
155
158
|
}
|
|
159
|
+
if (typeof next.notifyStaff !== 'boolean') throw new ApiError(400, 'rsvp.notifyStaff must be true or false.');
|
|
160
|
+
if (typeof next.waitlist !== 'boolean') throw new ApiError(400, 'rsvp.waitlist must be true or false.');
|
|
156
161
|
out.rsvp = next;
|
|
157
162
|
}
|
|
158
163
|
if (location !== undefined) out.location = mergeLocationInput(mergeEventDraft(before).location, location);
|
|
@@ -216,9 +221,12 @@ export async function cleanEventInput(repo: ContentRepository, data: EventSave,
|
|
|
216
221
|
|
|
217
222
|
export async function eventSideEffects(repo: ContentRepository, hooks: SaveHooks, before: CalEvent, after: CalEvent, applied = false) {
|
|
218
223
|
const settings = await repo.getSettings();
|
|
224
|
+
const origin = siteOrigin(settings, hooks.url);
|
|
219
225
|
await eventChanged(repo, before, after, {
|
|
220
|
-
origin
|
|
226
|
+
origin, waitUntil: waitUntilFrom(hooks.locals), applied, onRedirect: hooks.onRedirect,
|
|
221
227
|
});
|
|
228
|
+
// seats an edit freed (a higher capacity) go to those in line now, not at the next sweep
|
|
229
|
+
if (after.rsvp.waitlist) await offerEventWaitlist(repo, after.id, { origin, locals: hooks.locals, waitUntil: waitUntilFrom(hooks.locals) });
|
|
222
230
|
}
|
|
223
231
|
|
|
224
232
|
export async function saveEvent(repo: ContentRepository, id: string, data: EventSave, hooks: SaveHooks): Promise<CalEvent> {
|
package/src/lib/drafts.ts
CHANGED
|
@@ -52,8 +52,8 @@ export function pickEventDraft(draft: Record<string, unknown>): Partial<Pick<Cal
|
|
|
52
52
|
const loc = EventLocation.safeParse(out.location);
|
|
53
53
|
if (loc.success) out.location = loc.data; else delete out.location;
|
|
54
54
|
}
|
|
55
|
-
// and before RSVP deadlines: none
|
|
56
|
-
if (out.rsvp) out.rsvp = { ...out.rsvp, closesAt: out.rsvp.closesAt ?? null, closeBefore: out.rsvp.closeBefore ?? 0 };
|
|
55
|
+
// and before RSVP deadlines: none; before staff RSVP emails: on; before waitlists: off
|
|
56
|
+
if (out.rsvp) out.rsvp = { ...out.rsvp, closesAt: out.rsvp.closesAt ?? null, closeBefore: out.rsvp.closeBefore ?? 0, notifyStaff: out.rsvp.notifyStaff ?? true, waitlist: out.rsvp.waitlist ?? false };
|
|
57
57
|
return out;
|
|
58
58
|
}
|
|
59
59
|
export function pickPageDraft(draft: Record<string, unknown>): Partial<Pick<Page, PageDraftKey>> {
|
|
@@ -11,10 +11,12 @@ import { pageQuery } from '../mobile/http';
|
|
|
11
11
|
// Bookings inbox for editor apps: the same status changes as Admin →
|
|
12
12
|
// Bookings (confirm, decline, cancel; the guest is emailed, webhooks fire),
|
|
13
13
|
// available to every signed-in owner or editor exactly like the web page.
|
|
14
|
+
// RSVPs on a full date's waitlist are waitlisted (in line) or offered (a spot
|
|
15
|
+
// held for them); cancel takes them off it.
|
|
14
16
|
|
|
15
17
|
export const InboxQuery = z.object({
|
|
16
|
-
/** active = pending + confirmed (default); pending requests always sort first */
|
|
17
|
-
status: z.enum(['active', 'pending', 'confirmed', 'declined', 'cancelled', 'all']).default('active'),
|
|
18
|
+
/** active = pending + confirmed (default); waitlist = waitlisted + offered; pending requests always sort first */
|
|
19
|
+
status: z.enum(['active', 'pending', 'confirmed', 'waitlist', 'waitlisted', 'offered', 'expired', 'declined', 'cancelled', 'all']).default('active'),
|
|
18
20
|
from: z.string().datetime({ offset: true }).optional(),
|
|
19
21
|
...pageQuery,
|
|
20
22
|
}).strict();
|
|
@@ -26,7 +28,7 @@ export function requireBookings() {
|
|
|
26
28
|
|
|
27
29
|
export async function inbox(repo: ContentRepository, q: z.infer<typeof InboxQuery>, now = Date.now()) {
|
|
28
30
|
requireBookings();
|
|
29
|
-
const statuses: BookingStatus[] | undefined = q.status === 'all' ? undefined : q.status === 'active' ? ['pending', 'confirmed'] : [q.status];
|
|
31
|
+
const statuses: BookingStatus[] | undefined = q.status === 'all' ? undefined : q.status === 'active' ? ['pending', 'confirmed'] : q.status === 'waitlist' ? ['waitlisted', 'offered'] : [q.status];
|
|
30
32
|
const [list, resources, events] = await Promise.all([
|
|
31
33
|
repo.listBookings({ from: q.from ? Date.parse(q.from) : now - 86_400_000, statuses, kinds: ['booking', 'rsvp'] }),
|
|
32
34
|
repo.listResources(), repo.listEvents(),
|
|
@@ -160,9 +160,11 @@ export const EventFields = z.object({ ...common, excerpt: text(1000), tags: z.ar
|
|
|
160
160
|
}).partial({ lat: true, lng: true, placeId: true, showMap: true, mapImage: true, mapImageDark: true }).strict(),
|
|
161
161
|
cta: z.object({ label: text(120), href: text(1000), style: z.enum(['primary', 'outline']) }).strict(),
|
|
162
162
|
calendarIds: z.array(text(100)).max(50), fields: z.record(text(2000)),
|
|
163
|
-
// closesAt: wall time in the event's zone ('YYYY-MM-DDTHH:mm'); closeBefore: minutes before each date
|
|
163
|
+
// closesAt: wall time in the event's zone ('YYYY-MM-DDTHH:mm'); closeBefore: minutes before each date;
|
|
164
|
+
// notifyStaff: the hosts get an email about each new or cancelled RSVP; waitlist: a full date
|
|
165
|
+
// takes places in line (with a capacity). Left out = kept
|
|
164
166
|
rsvp: z.object({ enabled: z.boolean(), capacity: z.number().int().min(0).max(100000), autoConfirm: z.boolean(), maxSeats: z.number().int().min(1).max(50),
|
|
165
|
-
closesAt: text(40).nullable().optional(), closeBefore: z.number().int().min(0).max(86400).optional(), form: FormDefinition.optional() }).strict(),
|
|
167
|
+
closesAt: text(40).nullable().optional(), closeBefore: z.number().int().min(0).max(86400).optional(), notifyStaff: z.boolean().optional(), waitlist: z.boolean().optional(), form: FormDefinition.optional() }).strict(),
|
|
166
168
|
}).partial().strict();
|
|
167
169
|
export const SaveInput = z.object({ revision: z.string().max(60), fields: z.record(z.unknown()) }).strict();
|
|
168
170
|
|
|
@@ -47,7 +47,7 @@ export const EditorDocument = EditorDocumentSummary.extend({
|
|
|
47
47
|
event: z.object({
|
|
48
48
|
start: str, end: str, allDay: bool, timezone: str, rrule: str, exdates: z.array(str), cancelled: bool,
|
|
49
49
|
location: z.object({ name: str, address: str, url: str, lat: num.nullable(), lng: num.nullable(), placeId: str, showMap: bool, mapImage: str, mapImageDark: str }), cta: z.object({ label: str, href: str, style: z.enum(['primary', 'outline']) }),
|
|
50
|
-
calendarIds: z.array(str), fields: z.record(str), rsvp: z.object({ enabled: bool, capacity: num, autoConfirm: bool, maxSeats: num, closesAt: str.nullable(), closeBefore: num, form: FormDefinition.optional() }),
|
|
50
|
+
calendarIds: z.array(str), fields: z.record(str), rsvp: z.object({ enabled: bool, capacity: num, autoConfirm: bool, maxSeats: num, closesAt: str.nullable(), closeBefore: num, notifyStaff: bool, waitlist: bool, form: FormDefinition.optional() }),
|
|
51
51
|
}).nullable(),
|
|
52
52
|
languages: z.array(str),
|
|
53
53
|
});
|
|
@@ -3,10 +3,12 @@
|
|
|
3
3
|
// (book/manage; an RSVP's page words are in calendar/rsvp-words.ts). Emails
|
|
4
4
|
// say usted in Spanish, like the privacy notice; the website's pages say tú,
|
|
5
5
|
// like the RSVP words. Plain strings with {name}, {title}, {when}, {ref},
|
|
6
|
-
// {url} and {hours} slots
|
|
6
|
+
// {url} and {hours} slots, and {position} and {until} for the waitlist
|
|
7
|
+
// (live/words.ts `fill`). `rsvp` rewords a few
|
|
7
8
|
// messages for a seat at an event (English says "booking" for both). Staff
|
|
8
9
|
// emails use the site's main language. Other languages fall back to English.
|
|
9
|
-
|
|
10
|
+
// waitlisted, offered, left: an RSVP's waitlist (events only), worded for it
|
|
11
|
+
export type GuestMail = 'received' | 'confirmed' | 'declined' | 'cancelled' | 'updated' | 'rescheduled' | 'reminder' | 'link' | 'waitlisted' | 'offered' | 'left';
|
|
10
12
|
export interface MailCopy { subject: string; heading: string; intro: string }
|
|
11
13
|
|
|
12
14
|
export interface BookingStrings {
|
|
@@ -14,6 +16,8 @@ export interface BookingStrings {
|
|
|
14
16
|
/** the button to the booking's own page */
|
|
15
17
|
manage: string;
|
|
16
18
|
rsvp: { manage: string } & Partial<Record<GuestMail, Partial<MailCopy>>>;
|
|
19
|
+
/** the waitlist emails' buttons: the place in line (where they can leave it), and taking a held spot */
|
|
20
|
+
waitlist: { manage: string; confirm: string };
|
|
17
21
|
/** the details table */
|
|
18
22
|
what: string;
|
|
19
23
|
when: string;
|
|
@@ -25,6 +29,22 @@ export interface BookingStrings {
|
|
|
25
29
|
/** a pending booking nobody answered in time, declined automatically */
|
|
26
30
|
lateDecline: string;
|
|
27
31
|
staff: { pending: string; booked: string; review: string; open: string; pendingSubject: string; bookedSubject: string };
|
|
32
|
+
/**
|
|
33
|
+
* the hosts' email about an RSVP (or the contact address's, for an event
|
|
34
|
+
* without hosts): a new one, or one the person cancelled. {seats}, {count}
|
|
35
|
+
* and {capacity} are numbers, {date} the date's short form ("Fri Nov 7");
|
|
36
|
+
* the footer is `via` · `hosts` or `noHosts`.
|
|
37
|
+
*/
|
|
38
|
+
rsvpStaff: {
|
|
39
|
+
subject: string; cancelledSubject: string; heading: string; cancelledHeading: string;
|
|
40
|
+
name: string; email: string; phone: string; answers: string; now: string;
|
|
41
|
+
confirmed: string; pending: string; seatFreed: string; seatsFreed: string;
|
|
42
|
+
seatTaken: string; seatsTaken: string; seatsTakenOf: string;
|
|
43
|
+
via: Record<'web' | 'app' | 'staff', string>;
|
|
44
|
+
hosts: string; noHosts: string;
|
|
45
|
+
/** the button for a host without Manage bookings: their My events page */
|
|
46
|
+
openMine: string;
|
|
47
|
+
};
|
|
28
48
|
/** a room booking's own page: what differs from an RSVP's (rsvp-words.ts) */
|
|
29
49
|
page: {
|
|
30
50
|
untitled: string;
|
|
@@ -55,11 +75,23 @@ const STRINGS: Record<string, BookingStrings> = {
|
|
|
55
75
|
rescheduled: { subject: 'New time: {title} — {when}', heading: 'Booking moved', intro: 'Your booking has a new time. Import the attached calendar file to update your calendar.' },
|
|
56
76
|
reminder: { subject: 'Reminder: {title} — {when}', heading: 'Coming up soon', intro: 'A quick reminder about your booking.' },
|
|
57
77
|
link: { subject: 'Your booking: {title}', heading: 'You already have this booking', intro: 'We got another request for this date with your email address, so nothing changed. To see or change your booking, use the button below.' },
|
|
78
|
+
waitlisted: { subject: 'You’re on the waitlist: {title}', heading: 'You’re on the waitlist', intro: 'You’re #{position} in line. If a spot opens, we’ll email you and hold it for {hours} hours.' },
|
|
79
|
+
offered: { subject: 'A spot opened: {title}', heading: 'A spot opened for {title}', intro: 'We’re holding it for you until {until}. Confirm it before then, or it goes to the next person in line.' },
|
|
80
|
+
left: { subject: 'Off the waitlist: {title}', heading: 'You’re off the waitlist', intro: 'You’re no longer on the waitlist for this date.' },
|
|
58
81
|
},
|
|
59
82
|
manage: 'View or change your booking', rsvp: { manage: 'View or change your booking' },
|
|
83
|
+
waitlist: { manage: 'See your place or leave the waitlist', confirm: 'Confirm my spot' },
|
|
60
84
|
what: 'What', when: 'When', where: 'Where', seats: 'Seats', reference: 'Reference {ref}', icsManage: 'Manage: {url}',
|
|
61
85
|
lateDecline: 'We weren’t able to confirm this booking in time. Please pick another time.',
|
|
62
86
|
staff: { pending: 'New request to approve', booked: 'New booking', review: 'Review in the admin', open: 'Open in the admin', pendingSubject: 'Approve: {title} — {when}', bookedSubject: 'New booking: {title} — {when}' },
|
|
87
|
+
rsvpStaff: {
|
|
88
|
+
subject: 'New RSVP: {title}, {date}', cancelledSubject: 'RSVP cancelled: {title}, {date}', heading: 'New RSVP for {title}', cancelledHeading: 'RSVP cancelled for {title}',
|
|
89
|
+
name: 'Name', email: 'Email', phone: 'Phone', answers: 'Answers', now: 'Now',
|
|
90
|
+
confirmed: '{seats} · Confirmed', pending: '{seats} · Waiting for your approval', seatFreed: '{seats} freed', seatsFreed: '{seats} freed',
|
|
91
|
+
seatTaken: '{count} seat taken', seatsTaken: '{count} seats taken', seatsTakenOf: '{count} of {capacity} seats taken',
|
|
92
|
+
via: { web: 'Sent from the website', app: 'Sent from the app', staff: 'Added by staff' }, openMine: 'Open in My events',
|
|
93
|
+
hosts: 'You get these because you host this event.', noHosts: 'You get these because no one hosts this event.',
|
|
94
|
+
},
|
|
63
95
|
page: {
|
|
64
96
|
untitled: 'Booking', booked: 'You’re booked', yours: 'Your booking', moved: 'Booking moved', notAvailable: 'Not available', cancelledTitle: 'Booking cancelled',
|
|
65
97
|
declined: 'Sorry — this time couldn’t be confirmed. You’re welcome to pick another.', cancelledText: 'This booking is cancelled.',
|
|
@@ -79,6 +111,9 @@ const STRINGS: Record<string, BookingStrings> = {
|
|
|
79
111
|
rescheduled: { subject: 'Nuevo horario: {title} — {when}', heading: 'Su reservación cambió de horario', intro: 'Su reservación tiene un nuevo horario. Importe el archivo de calendario adjunto para actualizar su calendario.' },
|
|
80
112
|
reminder: { subject: 'Recordatorio: {title} — {when}', heading: 'Ya casi es la fecha', intro: 'Un breve recordatorio de su reservación.' },
|
|
81
113
|
link: { subject: 'Su reservación: {title}', heading: 'Ya tiene esta reservación', intro: 'Recibimos otra solicitud para este horario con su correo, así que no cambió nada. Para ver o cambiar su reservación, use el botón de abajo.' },
|
|
114
|
+
waitlisted: { subject: 'Está en la lista de espera: {title}', heading: 'Está en la lista de espera', intro: 'Tiene el lugar #{position} en la fila. Si se libera un lugar, le escribiremos y se lo apartaremos {hours} horas.' },
|
|
115
|
+
offered: { subject: 'Se liberó un lugar: {title}', heading: 'Se liberó un lugar para {title}', intro: 'Se lo apartamos hasta el {until}. Confírmelo antes, o pasará a la siguiente persona en la fila.' },
|
|
116
|
+
left: { subject: 'Fuera de la lista de espera: {title}', heading: 'Ya no está en la lista de espera', intro: 'Ya no está en la lista de espera para esta fecha.' },
|
|
82
117
|
},
|
|
83
118
|
manage: 'Ver o cambiar su reservación',
|
|
84
119
|
rsvp: {
|
|
@@ -90,9 +125,18 @@ const STRINGS: Record<string, BookingStrings> = {
|
|
|
90
125
|
reminder: { intro: 'Un breve recordatorio: tiene un lugar apartado.' },
|
|
91
126
|
link: { subject: 'Su confirmación para {title}', heading: 'Ya tiene una confirmación', intro: 'Recibimos otra confirmación para esta fecha con su correo, así que no cambió nada. Para cambiar la suya, o agregar lugares, use el botón de abajo.' },
|
|
92
127
|
},
|
|
128
|
+
waitlist: { manage: 'Ver su lugar o salir de la lista', confirm: 'Confirmar mi lugar' },
|
|
93
129
|
what: 'Qué', when: 'Cuándo', where: 'Dónde', seats: 'Lugares', reference: 'Referencia {ref}', icsManage: 'Ver o cambiar: {url}',
|
|
94
130
|
lateDecline: 'No pudimos confirmar esta reservación a tiempo. Por favor, elija otro horario.',
|
|
95
131
|
staff: { pending: 'Nueva solicitud por aprobar', booked: 'Nueva reservación', review: 'Revisar en el panel', open: 'Abrir en el panel', pendingSubject: 'Por aprobar: {title} — {when}', bookedSubject: 'Nueva reservación: {title} — {when}' },
|
|
132
|
+
rsvpStaff: {
|
|
133
|
+
subject: 'Nueva confirmación: {title}, {date}', cancelledSubject: 'Confirmación cancelada: {title}, {date}', heading: 'Nueva confirmación para {title}', cancelledHeading: 'Confirmación cancelada para {title}',
|
|
134
|
+
name: 'Nombre', email: 'Correo', phone: 'Teléfono', answers: 'Respuestas', now: 'Ahora',
|
|
135
|
+
confirmed: '{seats} · Confirmado', pending: '{seats} · Esperando su aprobación', seatFreed: '{seats} liberado', seatsFreed: '{seats} liberados',
|
|
136
|
+
seatTaken: '{count} lugar ocupado', seatsTaken: '{count} lugares ocupados', seatsTakenOf: '{count} de {capacity} lugares ocupados',
|
|
137
|
+
via: { web: 'Enviado desde el sitio web', app: 'Enviado desde la app', staff: 'Agregado por el equipo' }, openMine: 'Abrir en Mis eventos',
|
|
138
|
+
hosts: 'Recibe estos correos porque usted organiza este evento.', noHosts: 'Recibe estos correos porque este evento no tiene organizadores.',
|
|
139
|
+
},
|
|
96
140
|
page: {
|
|
97
141
|
untitled: 'Reservación', booked: 'Tu reservación está confirmada', yours: 'Tu reservación', moved: 'Tu reservación cambió de horario', notAvailable: 'No disponible', cancelledTitle: 'Reservación cancelada',
|
|
98
142
|
declined: 'Lo sentimos, no se pudo confirmar este horario. Puedes elegir otro.', cancelledText: 'Esta reservación se canceló.',
|
|
@@ -112,6 +156,9 @@ const STRINGS: Record<string, BookingStrings> = {
|
|
|
112
156
|
rescheduled: { subject: 'Nouvel horaire : {title} — {when}', heading: 'Réservation déplacée', intro: 'Votre réservation a un nouvel horaire. Importez le fichier de calendrier joint pour mettre à jour votre calendrier.' },
|
|
113
157
|
reminder: { subject: 'Rappel : {title} — {when}', heading: 'C’est bientôt', intro: 'Un petit rappel de votre réservation.' },
|
|
114
158
|
link: { subject: 'Votre réservation : {title}', heading: 'Vous avez déjà cette réservation', intro: 'Nous avons reçu une autre demande pour ce créneau avec votre adresse e-mail, donc rien n’a changé. Pour voir ou modifier votre réservation, utilisez le bouton ci-dessous.' },
|
|
159
|
+
waitlisted: { subject: 'Vous êtes sur la liste d’attente : {title}', heading: 'Vous êtes sur la liste d’attente', intro: 'Vous êtes n° {position} dans la file. Si une place se libère, nous vous écrirons et vous la garderons {hours} heures.' },
|
|
160
|
+
offered: { subject: 'Une place s’est libérée : {title}', heading: 'Une place s’est libérée pour {title}', intro: 'Nous vous la gardons jusqu’au {until}. Confirmez-la d’ici là, sinon elle ira à la personne suivante dans la file.' },
|
|
161
|
+
left: { subject: 'Hors de la liste d’attente : {title}', heading: 'Vous n’êtes plus sur la liste d’attente', intro: 'Vous n’êtes plus sur la liste d’attente pour cette date.' },
|
|
115
162
|
},
|
|
116
163
|
manage: 'Voir ou modifier votre réservation',
|
|
117
164
|
rsvp: {
|
|
@@ -123,9 +170,18 @@ const STRINGS: Record<string, BookingStrings> = {
|
|
|
123
170
|
reminder: { intro: 'Un petit rappel de votre inscription.' },
|
|
124
171
|
link: { subject: 'Votre inscription : {title}', heading: 'Vous avez déjà une inscription', intro: 'Nous avons reçu une autre inscription pour cette date avec votre adresse e-mail, donc rien n’a changé. Pour modifier la vôtre ou ajouter des places, utilisez le bouton ci-dessous.' },
|
|
125
172
|
},
|
|
173
|
+
waitlist: { manage: 'Voir votre place ou quitter la liste', confirm: 'Confirmer ma place' },
|
|
126
174
|
what: 'Quoi', when: 'Quand', where: 'Où', seats: 'Places', reference: 'Référence {ref}', icsManage: 'Gérer : {url}',
|
|
127
175
|
lateDecline: 'Nous n’avons pas pu confirmer cette réservation à temps. Veuillez choisir un autre horaire.',
|
|
128
176
|
staff: { pending: 'Nouvelle demande à valider', booked: 'Nouvelle réservation', review: 'Examiner dans l’administration', open: 'Ouvrir dans l’administration', pendingSubject: 'À valider : {title} — {when}', bookedSubject: 'Nouvelle réservation : {title} — {when}' },
|
|
177
|
+
rsvpStaff: {
|
|
178
|
+
subject: 'Nouvelle inscription : {title}, {date}', cancelledSubject: 'Inscription annulée : {title}, {date}', heading: 'Nouvelle inscription à {title}', cancelledHeading: 'Inscription à {title} annulée',
|
|
179
|
+
name: 'Nom', email: 'E-mail', phone: 'Téléphone', answers: 'Réponses', now: 'Maintenant',
|
|
180
|
+
confirmed: '{seats} · Confirmée', pending: '{seats} · En attente de votre validation', seatFreed: '{seats} libérée', seatsFreed: '{seats} libérées',
|
|
181
|
+
seatTaken: '{count} place prise', seatsTaken: '{count} places prises', seatsTakenOf: '{count} sur {capacity} places prises',
|
|
182
|
+
via: { web: 'Envoyée depuis le site', app: 'Envoyée depuis l’app', staff: 'Ajoutée par l’équipe' }, openMine: 'Ouvrir dans Mes événements',
|
|
183
|
+
hosts: 'Vous recevez ces e-mails parce que vous organisez cet événement.', noHosts: 'Vous recevez ces e-mails parce que cet événement n’a pas d’organisateur.',
|
|
184
|
+
},
|
|
129
185
|
page: {
|
|
130
186
|
untitled: 'Réservation', booked: 'Votre réservation est confirmée', yours: 'Votre réservation', moved: 'Réservation déplacée', notAvailable: 'Indisponible', cancelledTitle: 'Réservation annulée',
|
|
131
187
|
declined: 'Désolés, ce créneau n’a pas pu être confirmé. N’hésitez pas à en choisir un autre.', cancelledText: 'Cette réservation est annulée.',
|
|
@@ -145,6 +201,9 @@ const STRINGS: Record<string, BookingStrings> = {
|
|
|
145
201
|
rescheduled: { subject: 'Neuer Termin: {title} — {when}', heading: 'Buchung verschoben', intro: 'Ihre Buchung hat einen neuen Termin. Importieren Sie die angehängte Kalenderdatei, um Ihren Kalender zu aktualisieren.' },
|
|
146
202
|
reminder: { subject: 'Erinnerung: {title} — {when}', heading: 'Bald ist es so weit', intro: 'Eine kurze Erinnerung an Ihre Buchung.' },
|
|
147
203
|
link: { subject: 'Ihre Buchung: {title}', heading: 'Sie haben diese Buchung schon', intro: 'Wir haben eine weitere Anfrage für diesen Termin mit Ihrer E-Mail-Adresse erhalten, es hat sich also nichts geändert. Über den Button unten sehen oder ändern Sie Ihre Buchung.' },
|
|
204
|
+
waitlisted: { subject: 'Sie stehen auf der Warteliste: {title}', heading: 'Sie stehen auf der Warteliste', intro: 'Sie sind Nr. {position} auf der Warteliste. Wird ein Platz frei, schreiben wir Ihnen und halten ihn {hours} Stunden für Sie frei.' },
|
|
205
|
+
offered: { subject: 'Ein Platz ist frei geworden: {title}', heading: 'Ein Platz für {title} ist frei geworden', intro: 'Wir halten ihn bis {until} für Sie frei. Bestätigen Sie ihn bis dahin, sonst geht er an die nächste Person auf der Warteliste.' },
|
|
206
|
+
left: { subject: 'Nicht mehr auf der Warteliste: {title}', heading: 'Sie stehen nicht mehr auf der Warteliste', intro: 'Sie stehen für diesen Termin nicht mehr auf der Warteliste.' },
|
|
148
207
|
},
|
|
149
208
|
manage: 'Buchung ansehen oder ändern',
|
|
150
209
|
rsvp: {
|
|
@@ -156,9 +215,18 @@ const STRINGS: Record<string, BookingStrings> = {
|
|
|
156
215
|
reminder: { intro: 'Eine kurze Erinnerung an Ihre Anmeldung.' },
|
|
157
216
|
link: { subject: 'Ihre Anmeldung: {title}', heading: 'Sie sind schon angemeldet', intro: 'Wir haben eine weitere Anmeldung für diesen Termin mit Ihrer E-Mail-Adresse erhalten, es hat sich also nichts geändert. Über den Button unten ändern Sie Ihre Anmeldung oder fügen Plätze hinzu.' },
|
|
158
217
|
},
|
|
218
|
+
waitlist: { manage: 'Platz ansehen oder Warteliste verlassen', confirm: 'Platz bestätigen' },
|
|
159
219
|
what: 'Was', when: 'Wann', where: 'Wo', seats: 'Plätze', reference: 'Referenz {ref}', icsManage: 'Verwalten: {url}',
|
|
160
220
|
lateDecline: 'Wir konnten diese Buchung nicht rechtzeitig bestätigen. Bitte wählen Sie einen anderen Termin.',
|
|
161
221
|
staff: { pending: 'Neue Anfrage zur Freigabe', booked: 'Neue Buchung', review: 'In der Verwaltung prüfen', open: 'In der Verwaltung öffnen', pendingSubject: 'Freigeben: {title} — {when}', bookedSubject: 'Neue Buchung: {title} — {when}' },
|
|
222
|
+
rsvpStaff: {
|
|
223
|
+
subject: 'Neue Anmeldung: {title}, {date}', cancelledSubject: 'Anmeldung storniert: {title}, {date}', heading: 'Neue Anmeldung für {title}', cancelledHeading: 'Anmeldung für {title} storniert',
|
|
224
|
+
name: 'Name', email: 'E-Mail', phone: 'Telefon', answers: 'Antworten', now: 'Jetzt',
|
|
225
|
+
confirmed: '{seats} · Bestätigt', pending: '{seats} · Wartet auf Ihre Freigabe', seatFreed: '{seats} frei geworden', seatsFreed: '{seats} frei geworden',
|
|
226
|
+
seatTaken: '{count} Platz belegt', seatsTaken: '{count} Plätze belegt', seatsTakenOf: '{count} von {capacity} Plätzen belegt',
|
|
227
|
+
via: { web: 'Über die Website gesendet', app: 'Über die App gesendet', staff: 'Vom Team eingetragen' }, openMine: 'In Meine Veranstaltungen öffnen',
|
|
228
|
+
hosts: 'Sie erhalten diese E-Mails, weil Sie diese Veranstaltung betreuen.', noHosts: 'Sie erhalten diese E-Mails, weil niemand diese Veranstaltung betreut.',
|
|
229
|
+
},
|
|
162
230
|
page: {
|
|
163
231
|
untitled: 'Buchung', booked: 'Ihre Buchung ist bestätigt', yours: 'Ihre Buchung', moved: 'Buchung verschoben', notAvailable: 'Nicht verfügbar', cancelledTitle: 'Buchung storniert',
|
|
164
232
|
declined: 'Leider konnte dieser Termin nicht bestätigt werden. Wählen Sie gern einen anderen.', cancelledText: 'Diese Buchung ist storniert.',
|
|
@@ -178,6 +246,9 @@ const STRINGS: Record<string, BookingStrings> = {
|
|
|
178
246
|
rescheduled: { subject: 'Novo horário: {title} — {when}', heading: 'Reserva remarcada', intro: 'Sua reserva tem um novo horário. Importe o arquivo de calendário anexo para atualizar seu calendário.' },
|
|
179
247
|
reminder: { subject: 'Lembrete: {title} — {when}', heading: 'Está chegando', intro: 'Um lembrete rápido sobre sua reserva.' },
|
|
180
248
|
link: { subject: 'Sua reserva: {title}', heading: 'Você já tem esta reserva', intro: 'Recebemos outro pedido para este horário com seu e-mail, então nada mudou. Para ver ou alterar sua reserva, use o botão abaixo.' },
|
|
249
|
+
waitlisted: { subject: 'Você está na lista de espera: {title}', heading: 'Você está na lista de espera', intro: 'Você é o nº {position} da fila. Se uma vaga abrir, vamos avisar por e-mail e guardá-la por {hours} horas.' },
|
|
250
|
+
offered: { subject: 'Abriu uma vaga: {title}', heading: 'Abriu uma vaga em {title}', intro: 'Vamos guardá-la para você até {until}. Confirme antes disso, ou ela passa para a próxima pessoa da fila.' },
|
|
251
|
+
left: { subject: 'Fora da lista de espera: {title}', heading: 'Você não está mais na lista de espera', intro: 'Você não está mais na lista de espera desta data.' },
|
|
181
252
|
},
|
|
182
253
|
manage: 'Ver ou alterar sua reserva',
|
|
183
254
|
rsvp: {
|
|
@@ -189,9 +260,18 @@ const STRINGS: Record<string, BookingStrings> = {
|
|
|
189
260
|
reminder: { intro: 'Um lembrete rápido sobre sua inscrição.' },
|
|
190
261
|
link: { subject: 'Sua inscrição em {title}', heading: 'Você já tem uma inscrição', intro: 'Recebemos outra inscrição para esta data com seu e-mail, então nada mudou. Para alterar a sua, ou adicionar vagas, use o botão abaixo.' },
|
|
191
262
|
},
|
|
263
|
+
waitlist: { manage: 'Ver sua posição ou sair da lista', confirm: 'Confirmar minha vaga' },
|
|
192
264
|
what: 'O quê', when: 'Quando', where: 'Onde', seats: 'Vagas', reference: 'Referência {ref}', icsManage: 'Gerenciar: {url}',
|
|
193
265
|
lateDecline: 'Não conseguimos confirmar esta reserva a tempo. Por favor, escolha outro horário.',
|
|
194
266
|
staff: { pending: 'Novo pedido para aprovar', booked: 'Nova reserva', review: 'Revisar no painel', open: 'Abrir no painel', pendingSubject: 'Aprovar: {title} — {when}', bookedSubject: 'Nova reserva: {title} — {when}' },
|
|
267
|
+
rsvpStaff: {
|
|
268
|
+
subject: 'Nova inscrição: {title}, {date}', cancelledSubject: 'Inscrição cancelada: {title}, {date}', heading: 'Nova inscrição em {title}', cancelledHeading: 'Inscrição cancelada em {title}',
|
|
269
|
+
name: 'Nome', email: 'E-mail', phone: 'Telefone', answers: 'Respostas', now: 'Agora',
|
|
270
|
+
confirmed: '{seats} · Confirmada', pending: '{seats} · Aguardando sua aprovação', seatFreed: '{seats} liberada', seatsFreed: '{seats} liberadas',
|
|
271
|
+
seatTaken: '{count} vaga ocupada', seatsTaken: '{count} vagas ocupadas', seatsTakenOf: '{count} de {capacity} vagas ocupadas',
|
|
272
|
+
via: { web: 'Enviada pelo site', app: 'Enviada pelo app', staff: 'Adicionada pela equipe' }, openMine: 'Abrir em Meus eventos',
|
|
273
|
+
hosts: 'Você recebe estes e-mails porque organiza este evento.', noHosts: 'Você recebe estes e-mails porque este evento não tem organizadores.',
|
|
274
|
+
},
|
|
195
275
|
page: {
|
|
196
276
|
untitled: 'Reserva', booked: 'Sua reserva está confirmada', yours: 'Sua reserva', moved: 'Reserva remarcada', notAvailable: 'Indisponível', cancelledTitle: 'Reserva cancelada',
|
|
197
277
|
declined: 'Desculpe, não foi possível confirmar este horário. Fique à vontade para escolher outro.', cancelledText: 'Esta reserva foi cancelada.',
|
package/src/lib/email/service.ts
CHANGED
|
@@ -22,7 +22,7 @@ async function render(repo: ContentRepository, job: EmailJob, locals?: unknown,
|
|
|
22
22
|
const b = await repo.getBooking(payload.bookingId);
|
|
23
23
|
if (!b || (b.notificationVersion ?? 0) !== payload.version) return null;
|
|
24
24
|
if (payload.kind === 'reminder' && (b.status !== 'confirmed' || b.start <= Date.now())) return null;
|
|
25
|
-
const mail = await bookingMail(repo, b, payload.kind, origin, from, payload);
|
|
25
|
+
const mail = await bookingMail(repo, b, payload.kind, origin, from, payload, job.recipient);
|
|
26
26
|
return { ...mail, to: job.recipient };
|
|
27
27
|
}
|
|
28
28
|
if (job.kind === 'auth') {
|
package/src/lib/email/store.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { SqlDriver, SqlStatement } from '../repo/sql';
|
|
2
|
-
import type { ConfigRecord, EmailConfig, EmailJob, Invitation, NewEmailJob, SendResult } from './types';
|
|
2
|
+
import type { ConfigRecord, EmailConfig, EmailJob, Invitation, NewEmailJob, RsvpStaffNotice, SendResult } from './types';
|
|
3
3
|
const record = (r: Record<string, unknown>): ConfigRecord => ({ version: Number(r.version), config: JSON.parse(String(r.config)), encrypted: String(r.encrypted), createdAt: Number(r.created_at) });
|
|
4
4
|
function job(r: Record<string, unknown>): EmailJob {
|
|
5
5
|
return { id: String(r.id), dedupe: String(r.dedupe), kind: String(r.kind), recipient: String(r.recipient), payload: String(r.payload), configVersion: Number(r.config_version), state: r.state as EmailJob['state'], attempts: Number(r.attempts), nextAt: Number(r.next_at), expiresAt: r.expires_at == null ? null : Number(r.expires_at), createdAt: Number(r.created_at), lease: String(r.lease), leaseUntil: r.lease_until == null ? null : Number(r.lease_until), provider: String(r.provider), providerId: String(r.provider_id), error: String(r.error), acceptedAt: r.accepted_at == null ? null : Number(r.accepted_at), updatedAt: Number(r.updated_at), fingerprint: String(r.fingerprint), rendered: String(r.rendered || '') };
|
|
@@ -25,6 +25,22 @@ export function bookingJobs(id: string, kind: string, version?: number, staff =
|
|
|
25
25
|
});
|
|
26
26
|
return [select('b.email', 'bookings b', kind), ...(staff ? [select('j.value', 'bookings b JOIN resources r ON r.id = b.resource_id, json_each(r.notify_emails) j', 'staff-new')] : [])];
|
|
27
27
|
}
|
|
28
|
+
// Staff hear about an RSVP (new, or cancelled by the person) at the addresses
|
|
29
|
+
// the caller picked, in the same batch and only if the change landed.
|
|
30
|
+
// `taken` is the date's seat count right after it (held waitlist spots too).
|
|
31
|
+
export function rsvpStaffJobs(id: string, kind: 'staff-rsvp' | 'staff-rsvp-cancelled', notice: RsvpStaffNotice | undefined, version: number, mutation = ''): SqlStatement[] {
|
|
32
|
+
if (!notice?.to.length) return [];
|
|
33
|
+
const now = Date.now();
|
|
34
|
+
return [{
|
|
35
|
+
sql: `INSERT OR IGNORE INTO email_jobs (id,dedupe,kind,recipient,payload,config_version,next_at,expires_at,created_at,updated_at)
|
|
36
|
+
SELECT lower(hex(randomblob(16))), b.id || ':' || b.notification_version || ':' || ? || ':' || lower(j.value), 'booking', lower(j.value),
|
|
37
|
+
json_object('bookingId',b.id,'version',b.notification_version,'kind',?,'why',?,'via',?,
|
|
38
|
+
'taken',(SELECT COALESCE(SUM(seats),0) FROM bookings WHERE kind='rsvp' AND event_id=b.event_id AND start_at=b.start_at AND status IN ('pending','confirmed','offered'))),
|
|
39
|
+
COALESCE((SELECT MAX(version) FROM email_configs),0), ?, NULL, ?, ?
|
|
40
|
+
FROM bookings b, json_each(?) j WHERE b.id = ? AND b.kind = 'rsvp' AND j.value != '' AND b.notification_version = ? ${mutation ? 'AND b.email_mutation = ?' : ''}`,
|
|
41
|
+
args: [kind, kind, notice.why, notice.via, now, now, now, JSON.stringify(notice.to), id, version, ...(mutation ? [mutation] : [])],
|
|
42
|
+
}];
|
|
43
|
+
}
|
|
28
44
|
export class EmailStore {
|
|
29
45
|
constructor(private getDriver: () => Promise<SqlDriver>) {}
|
|
30
46
|
async config(version?: number): Promise<ConfigRecord | null> {
|
package/src/lib/email/types.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Role } from '../schema';
|
|
1
|
+
import type { Booking, Role } from '../schema';
|
|
2
2
|
export type Provider = 'resend' | 'mailgun' | 'ses' | 'cloudflare';
|
|
3
3
|
export type ProviderChoice = Provider | 'auto' | 'none';
|
|
4
4
|
export interface Attachment { filename: string; type: string; content: string }
|
|
@@ -19,6 +19,8 @@ export interface EmailJob {
|
|
|
19
19
|
acceptedAt: number | null; updatedAt: number; fingerprint: string;
|
|
20
20
|
rendered: string;
|
|
21
21
|
}
|
|
22
|
+
/** staff to tell about an RSVP (booking/service.ts rsvpStaff): to its hosts, else the contact address; via: where the person did it */
|
|
23
|
+
export interface RsvpStaffNotice { to: string[]; why: 'hosts' | 'contact'; via: Booking['source'] }
|
|
22
24
|
export interface NewEmailJob { id?: string; dedupe: string; kind: string; recipient: string; payload: string; configVersion: number; nextAt?: number; expiresAt?: number }
|
|
23
25
|
export type SendResult = { state: 'accepted'; providerId: string } | { state: 'retrying' | 'failed' | 'unknown'; error: string; retryAfter?: number };
|
|
24
26
|
export interface Invitation { id: string; email: string; name: string; role: Role; state: string; expiresAt: number; createdAt: number; invitedBy: string }
|
package/src/lib/forms/events.ts
CHANGED
|
@@ -17,7 +17,9 @@ export async function prepareLegacyEventForm(event: CalEvent, start: number, dat
|
|
|
17
17
|
}
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
// locale: the page's language, so the RSVP's emails and links speak it
|
|
20
|
+
// locale: the page's language, so the RSVP's emails and links speak it. A
|
|
21
|
+
// form with waitlist=1 (a full date's "Join the waitlist") takes a place in
|
|
22
|
+
// line when there's no room
|
|
21
23
|
export async function submitEventForm(repo: ContentRepository, event: CalEvent, occurrence: { start: number; end: number }, data: FormData, requestId: string, flow: Parameters<typeof createRsvp>[2], locale = '') {
|
|
22
24
|
if (event.status !== 'published') throw new ApiError(404, 'Event not found.');
|
|
23
25
|
if (Number(data.get('occurrence')) !== occurrence.start) throw new ApiError(409, 'The event time has changed. Review the date and try again.');
|
|
@@ -37,6 +39,6 @@ export async function submitEventForm(repo: ContentRepository, event: CalEvent,
|
|
|
37
39
|
const existing = await replay(); if (existing) return existing;
|
|
38
40
|
await collectResponse(definition, booker.answers, booker.formVersion);
|
|
39
41
|
// the language isn't part of the retry check: it doesn't change what was asked for
|
|
40
|
-
try { return await createRsvp(repo, { event, ...occurrence, seats, booker: { ...booker, locale } }, { ...flow, idempotency }); }
|
|
42
|
+
try { return await createRsvp(repo, { event, ...occurrence, seats, booker: { ...booker, locale }, waitlist: data.get('waitlist') === '1' }, { ...flow, idempotency }); }
|
|
41
43
|
catch (e) { const existing = await replay(); if (existing) return existing; throw e; }
|
|
42
44
|
}
|
|
@@ -10,8 +10,8 @@ export interface AppPrivacyAnswers { rows: StoreRow[]; tracking: string; notes:
|
|
|
10
10
|
|
|
11
11
|
export function appPrivacyAnswers(f: LegalFacts, privacyUrl: string): AppPrivacyAnswers {
|
|
12
12
|
const rows: StoreRow[] = [];
|
|
13
|
-
// apps can RSVP wherever the calendar is on; room bookings need bookings too
|
|
14
|
-
const contact = [...(f.bookings ? ['bookings'] : []), ...(f.calendar ? ['RSVPs'] : []), ...(f.appForms ? ['forms'] : [])];
|
|
13
|
+
// apps can RSVP (or join a full date's waitlist) wherever the calendar is on; room bookings need bookings too
|
|
14
|
+
const contact = [...(f.bookings ? ['bookings'] : []), ...(f.calendar ? ['RSVPs (including waitlist requests)'] : []), ...(f.appForms ? ['forms'] : [])];
|
|
15
15
|
const sends = contact.length > 1 ? `${contact.slice(0, -1).join(', ')} or ${contact.at(-1)}` : contact[0];
|
|
16
16
|
if (contact.length) {
|
|
17
17
|
rows.push({
|
package/src/lib/legal/text-en.ts
CHANGED
|
@@ -42,7 +42,7 @@ export function privacyEn(f: LegalFacts, country: string): PolicySection[] {
|
|
|
42
42
|
] },
|
|
43
43
|
];
|
|
44
44
|
if (f.calendar) s.push({ id: 'bookings', title: f.bookings ? 'Bookings and RSVPs' : 'Event RSVPs', text: [
|
|
45
|
-
`When you ${f.bookings ? 'book a time or ' : ''}RSVP to an event, we store your name, email address, any phone number and answers you give, the date and time, the number of seats and the status. We use them to manage your request, prevent duplicate submissions, and email you confirmations, changes and a reminder. Staff who manage the calendar can see them${f.bookings ? ', and the people a room or service notifies receive new bookings by email' : ''}.`,
|
|
45
|
+
`When you ${f.bookings ? 'book a time or ' : ''}RSVP to an event, we store your name, email address, any phone number and answers you give, the date and time, the number of seats and the status. We use them to manage your request, prevent duplicate submissions, and email you confirmations, changes and a reminder. Staff who manage the calendar can see them${f.bookings ? ', and the people a room or service notifies receive new bookings by email' : ''}. When you RSVP or cancel an RSVP, the event’s hosts (or our staff) get an email with your RSVP details. If a date is full and you join its waitlist, we keep your request like an RSVP and email you if a spot opens; we hold that spot for 24 hours.`,
|
|
46
46
|
'Each booking has a private link to see, change or cancel it. Keep it private: anyone with the link can use it.',
|
|
47
47
|
] });
|
|
48
48
|
s.push({ id: 'forms', title: 'Forms', text: [
|
package/src/lib/legal/text-es.ts
CHANGED
|
@@ -43,7 +43,7 @@ export function privacyEs(f: LegalFacts, country: string): PolicySection[] {
|
|
|
43
43
|
] },
|
|
44
44
|
];
|
|
45
45
|
if (f.calendar) s.push({ id: 'bookings', title: f.bookings ? 'Reservaciones y registros' : 'Registros a eventos', text: [
|
|
46
|
-
`Cuando ${f.bookings ? 'reserva un horario o ' : ''}se registra a un evento, guardamos su nombre, su correo electrónico, el teléfono y las respuestas que nos dé, la fecha y hora, el número de lugares y el estado. Los usamos para gestionar su solicitud, evitar envíos duplicados y enviarle por correo confirmaciones, cambios y un recordatorio. El personal que administra el calendario puede verlos${f.bookings ? ', y las personas a las que avisa una sala o un servicio reciben las reservaciones nuevas por correo' : ''}.`,
|
|
46
|
+
`Cuando ${f.bookings ? 'reserva un horario o ' : ''}se registra a un evento, guardamos su nombre, su correo electrónico, el teléfono y las respuestas que nos dé, la fecha y hora, el número de lugares y el estado. Los usamos para gestionar su solicitud, evitar envíos duplicados y enviarle por correo confirmaciones, cambios y un recordatorio. El personal que administra el calendario puede verlos${f.bookings ? ', y las personas a las que avisa una sala o un servicio reciben las reservaciones nuevas por correo' : ''}. Cuando se registra a un evento o cancela su registro, los organizadores del evento (o nuestro personal) reciben un correo con los datos de su registro. Si una fecha está llena y usted se une a su lista de espera, guardamos su solicitud como un registro y le escribimos si se libera un lugar; se lo apartamos 24 horas.`,
|
|
47
47
|
'Cada reservación tiene un enlace privado para verla, cambiarla o cancelarla. Manténgalo en privado: cualquiera que lo tenga puede usarlo.',
|
|
48
48
|
] });
|
|
49
49
|
s.push({ id: 'forms', title: 'Formularios', text: [
|