@acms-subir/core 0.3.55 → 0.3.57
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 +29 -0
- package/cli/migrations.generated.mjs +16 -4
- package/docs/calendar.md +189 -2
- package/docs/mobile.md +3 -1
- package/docs/server.md +84 -1
- package/integration.mjs +34 -0
- package/mobile-template/client/AGENTS.md +1 -0
- package/mobile-template/client/README.md +1 -1
- package/mobile-template/recipes/event-request/files/app/request.tsx +305 -0
- package/mobile-template/recipes/event-request/files/components/DayPicker.tsx +66 -0
- package/mobile-template/recipes/event-request/files/components/RequestEventLink.tsx +19 -0
- package/mobile-template/recipes/event-request/files/components/RequestEventRow.tsx +20 -0
- package/mobile-template/recipes/event-request/recipe.json +21 -0
- package/package.json +1 -1
- package/src/actions/index.ts +52 -9
- package/src/components/EventPage.astro +6 -5
- package/src/components/MobileApp.tsx +2 -2
- package/src/components/calendar/CalendarBoard.tsx +6 -3
- package/src/components/editor/event/SingleDates.tsx +87 -17
- package/src/layouts/Admin.astro +7 -0
- package/src/lib/calendar/api.ts +23 -2
- package/src/lib/calendar/board.ts +3 -2
- package/src/lib/calendar/cards.ts +4 -4
- package/src/lib/calendar/change-migration.ts +17 -3
- package/src/lib/calendar/data.ts +9 -2
- package/src/lib/calendar/event-words.ts +8 -0
- package/src/lib/calendar/events.ts +128 -15
- package/src/lib/calendar/google/map.ts +1 -1
- package/src/lib/calendar/host-requests.ts +65 -10
- package/src/lib/calendar/ics-write.ts +65 -15
- package/src/lib/calendar/member-words.ts +14 -1
- package/src/lib/calendar/moves.ts +15 -9
- package/src/lib/calendar/request-admin.ts +212 -0
- package/src/lib/calendar/request-config.ts +8 -0
- package/src/lib/calendar/request-mail.ts +158 -0
- package/src/lib/calendar/request-page.ts +52 -0
- package/src/lib/calendar/request-service.ts +535 -0
- package/src/lib/calendar/request-store.ts +2 -2
- package/src/lib/calendar/request-words.ts +263 -0
- package/src/lib/calendar/schedule.ts +42 -8
- package/src/lib/calendar/segments.ts +522 -0
- package/src/lib/editor/schedule.ts +2 -2
- package/src/lib/email/service.ts +6 -1
- package/src/lib/legal/app-privacy.ts +2 -2
- package/src/lib/legal/facts.ts +4 -0
- package/src/lib/legal/text-en.ts +3 -0
- package/src/lib/legal/text-es.ts +3 -0
- package/src/lib/mobile/calendar.ts +1 -0
- package/src/lib/mobile/http.ts +2 -1
- package/src/lib/mobile/openapi.ts +8 -0
- package/src/lib/mobile/requests.ts +27 -0
- package/src/lib/mobile/schemas.ts +24 -6
- package/src/lib/mobile/site.ts +3 -0
- package/src/lib/push/service.ts +42 -10
- package/src/lib/push/types.ts +3 -1
- package/src/lib/repo/migrations.ts +5 -2
- package/src/lib/repo/sql.ts +24 -8
- package/src/lib/repo/types.ts +3 -1
- package/src/lib/scheduler.ts +3 -0
- package/src/lib/schema.ts +67 -2
- package/src/lib/sections.ts +1 -1
- package/src/lib/sync/catalog.ts +1 -0
- package/src/pages/admin/calendar/events/index.astro +11 -8
- package/src/pages/admin/calendar/requests.astro +298 -0
- package/src/pages/api/calendar/events/index.ts +2 -1
- package/src/pages/api/v1/requests/index.ts +6 -0
- package/src/pages/api/v1/requests/options.ts +7 -0
- package/src/pages/members/events/index.astro +84 -6
- package/src/pages/request/index.astro +182 -0
- package/src/server/index.ts +102 -4
- package/src/styles/editor.css +2 -0
- package/src/styles/site.css +29 -0
- package/src/virtual.d.ts +1 -0
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
import type { RequestRefusal } from './request-service';
|
|
2
|
+
|
|
3
|
+
// Words of event requests (lib/calendar/request-service.ts): the built-in
|
|
4
|
+
// form at acms({ requests.path }), the emails to the person who asked (in
|
|
5
|
+
// the language they asked in) and the staff alert (in the site's main
|
|
6
|
+
// language). Plain strings with {time}, {email}, {phone}, {count}, {title}
|
|
7
|
+
// and {when} slots (live/words.ts `fill`). Spanish uses usted, like the
|
|
8
|
+
// other emails. The app's screen takes its EN/ES words from @acms-subir/native
|
|
9
|
+
// (useStrings), whose tests check them against these. Other languages fall
|
|
10
|
+
// back to English.
|
|
11
|
+
|
|
12
|
+
export interface RequestStrings {
|
|
13
|
+
// the form
|
|
14
|
+
heading: string;
|
|
15
|
+
/** when the site set no notice of its own */
|
|
16
|
+
notice: string;
|
|
17
|
+
somethingNew: string;
|
|
18
|
+
change: string;
|
|
19
|
+
what: string;
|
|
20
|
+
type: string;
|
|
21
|
+
date: string;
|
|
22
|
+
starts: string;
|
|
23
|
+
ends: string;
|
|
24
|
+
repeats: string;
|
|
25
|
+
once: string;
|
|
26
|
+
weekly: string;
|
|
27
|
+
other: string;
|
|
28
|
+
otherHint: string;
|
|
29
|
+
room: string;
|
|
30
|
+
notSure: string;
|
|
31
|
+
/** the live room check: "{room} looks free then." / "{room} isn’t free then." */
|
|
32
|
+
free: string;
|
|
33
|
+
taken: string;
|
|
34
|
+
people: string;
|
|
35
|
+
message: string;
|
|
36
|
+
optional: string;
|
|
37
|
+
event: string;
|
|
38
|
+
pickEvent: string;
|
|
39
|
+
onDate: string;
|
|
40
|
+
onDateHint: string;
|
|
41
|
+
whatChange: string;
|
|
42
|
+
contact: string;
|
|
43
|
+
name: string;
|
|
44
|
+
email: string;
|
|
45
|
+
phone: string;
|
|
46
|
+
contactWhy: string;
|
|
47
|
+
privacy: string;
|
|
48
|
+
send: string;
|
|
49
|
+
closed: string;
|
|
50
|
+
/** after `closed`, with the site's phone */
|
|
51
|
+
call: string;
|
|
52
|
+
// sent
|
|
53
|
+
sent: string;
|
|
54
|
+
/** with the site's reply time ("3 days") */
|
|
55
|
+
answerWithin: string;
|
|
56
|
+
answerBy: string;
|
|
57
|
+
yours: string;
|
|
58
|
+
notYet: string;
|
|
59
|
+
sooner: string;
|
|
60
|
+
another: string;
|
|
61
|
+
/** "about {count} people" */
|
|
62
|
+
aboutPeople: string;
|
|
63
|
+
/** "Change to {title}" */
|
|
64
|
+
changeTo: string;
|
|
65
|
+
everyDate: string;
|
|
66
|
+
errors: Record<RequestRefusal | 'failed', string>;
|
|
67
|
+
// the emails to the person
|
|
68
|
+
mail: {
|
|
69
|
+
receivedSubject: string;
|
|
70
|
+
received: string;
|
|
71
|
+
/** after `received` when the site gave a reply time */
|
|
72
|
+
within: string;
|
|
73
|
+
replySubject: string;
|
|
74
|
+
reply: string;
|
|
75
|
+
approvedSubject: string;
|
|
76
|
+
approved: string;
|
|
77
|
+
approvedChange: string;
|
|
78
|
+
declinedSubject: string;
|
|
79
|
+
declined: string;
|
|
80
|
+
declinedChange: string;
|
|
81
|
+
fromTeam: string;
|
|
82
|
+
notYou: string;
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const STRINGS: Record<string, RequestStrings> = {
|
|
87
|
+
en: {
|
|
88
|
+
heading: 'Request an event', notice: 'We review every request. It isn’t on the calendar until we confirm it.',
|
|
89
|
+
somethingNew: 'Something new', change: 'Change to an event', what: 'What are you planning?', type: 'Kind', date: 'Date', starts: 'Starts', ends: 'Ends',
|
|
90
|
+
repeats: 'Repeats', once: 'Just once', weekly: 'Every week', other: 'Something else', otherHint: 'Tell us how it repeats',
|
|
91
|
+
room: 'Room', notSure: 'Not sure', free: '{room} looks free then.', taken: '{room} isn’t free then.', people: 'How many people?', message: 'Anything else we should know?', optional: 'Optional',
|
|
92
|
+
event: 'Which event?', pickEvent: 'Choose an event', onDate: 'Which date?', onDateHint: 'Leave it empty if it’s about every date.', whatChange: 'What should change?',
|
|
93
|
+
contact: 'How we reach you', name: 'Your name', email: 'Email', phone: 'Phone', contactWhy: 'We use this only to answer your request.', privacy: 'Privacy',
|
|
94
|
+
send: 'Send request', closed: 'We aren’t taking requests here right now.', call: 'Call us at {phone}.',
|
|
95
|
+
sent: 'Request sent', answerWithin: 'We usually answer within {time}, by email to {email}.', answerBy: 'We’ll answer by email to {email}.',
|
|
96
|
+
yours: 'Your request', notYet: 'It isn’t on the calendar until we confirm it.', sooner: 'Need it sooner? Call {phone}.', another: 'Send another request',
|
|
97
|
+
aboutPeople: 'about {count} people', changeTo: 'Change to {title}', everyDate: 'every date',
|
|
98
|
+
errors: {
|
|
99
|
+
closed: 'We aren’t taking requests here right now.', trap: 'Couldn’t send this request.', busy: 'Too many requests. Try again in a minute.',
|
|
100
|
+
full: 'We’ve had a lot of requests today. Please try again tomorrow.', changed: 'This request was already sent. Reload the page to send a new one.',
|
|
101
|
+
title: 'Say what you’re planning.', type: 'Choose a kind from the list.', start: 'Pick a date and a start time.', end: 'Pick an end time within a day of the start.',
|
|
102
|
+
past: 'Pick a time that hasn’t passed.', far: 'Pick a date within the next two years.', repeat: 'Tell us how it repeats.', room: 'Choose a room from the list.',
|
|
103
|
+
people: 'Check the number of people.', event: 'Choose an event from the list.', date: 'That event doesn’t happen on that date.', what: 'Tell us what should change.',
|
|
104
|
+
name: 'Add your name.', email: 'Add a valid email address.', phone: 'Check the phone number, or leave it empty.', failed: 'Couldn’t send your request. Please try again.',
|
|
105
|
+
},
|
|
106
|
+
mail: {
|
|
107
|
+
receivedSubject: 'We got your request', received: 'We got your event request. It isn’t on the calendar until we confirm it.', within: 'We usually answer within {time}.',
|
|
108
|
+
replySubject: 'About your event request', reply: 'We wrote about your request:',
|
|
109
|
+
approvedSubject: 'Your request was approved', approved: 'We approved your event request. We’ll be in touch if we need anything else.', approvedChange: 'We approved your request, and we’ll make the change.',
|
|
110
|
+
declinedSubject: 'About your event request', declined: 'We’re sorry, we can’t take your event request.', declinedChange: 'We’re sorry, we can’t make the change you asked for.',
|
|
111
|
+
fromTeam: 'From the team:', notYou: 'If you didn’t send this request, you can ignore this email.',
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
es: {
|
|
115
|
+
heading: 'Solicitar un evento', notice: 'Revisamos cada solicitud. No estará en el calendario hasta que la confirmemos.',
|
|
116
|
+
somethingNew: 'Algo nuevo', change: 'Un cambio a un evento', what: '¿Qué está planeando?', type: 'Tipo', date: 'Fecha', starts: 'Comienza', ends: 'Termina',
|
|
117
|
+
repeats: 'Se repite', once: 'Solo una vez', weekly: 'Cada semana', other: 'Otra cosa', otherHint: 'Díganos cómo se repite',
|
|
118
|
+
room: 'Salón', notSure: 'No estoy seguro', free: '{room}: libre a esa hora.', taken: '{room}: no está libre a esa hora.', people: '¿Cuántas personas?', message: '¿Algo más que debamos saber?', optional: 'Opcional',
|
|
119
|
+
event: '¿Qué evento?', pickEvent: 'Elija un evento', onDate: '¿Qué fecha?', onDateHint: 'Déjelo vacío si es sobre todas las fechas.', whatChange: '¿Qué debe cambiar?',
|
|
120
|
+
contact: 'Cómo le contactamos', name: 'Su nombre', email: 'Correo electrónico', phone: 'Teléfono', contactWhy: 'Solo lo usamos para responder a su solicitud.', privacy: 'Privacidad',
|
|
121
|
+
send: 'Enviar solicitud', closed: 'Por ahora no recibimos solicitudes aquí.', call: 'Llámenos al {phone}.',
|
|
122
|
+
sent: 'Solicitud enviada', answerWithin: 'Normalmente respondemos en {time}, por correo a {email}.', answerBy: 'Le responderemos por correo a {email}.',
|
|
123
|
+
yours: 'Su solicitud', notYet: 'No estará en el calendario hasta que la confirmemos.', sooner: '¿La necesita antes? Llame al {phone}.', another: 'Enviar otra solicitud',
|
|
124
|
+
aboutPeople: 'unas {count} personas', changeTo: 'Cambio a {title}', everyDate: 'todas las fechas',
|
|
125
|
+
errors: {
|
|
126
|
+
closed: 'Por ahora no recibimos solicitudes aquí.', trap: 'No se pudo enviar esta solicitud.', busy: 'Demasiadas solicitudes. Inténtelo de nuevo en un minuto.',
|
|
127
|
+
full: 'Hoy hemos recibido muchas solicitudes. Inténtelo de nuevo mañana.', changed: 'Esta solicitud ya se envió. Vuelva a cargar la página para enviar otra.',
|
|
128
|
+
title: 'Diga qué está planeando.', type: 'Elija un tipo de la lista.', start: 'Elija una fecha y una hora de inicio.', end: 'Elija una hora de fin dentro de un día desde el inicio.',
|
|
129
|
+
past: 'Elija una hora que no haya pasado.', far: 'Elija una fecha dentro de los próximos dos años.', repeat: 'Díganos cómo se repite.', room: 'Elija un salón de la lista.',
|
|
130
|
+
people: 'Revise el número de personas.', event: 'Elija un evento de la lista.', date: 'Ese evento no ocurre en esa fecha.', what: 'Díganos qué debe cambiar.',
|
|
131
|
+
name: 'Escriba su nombre.', email: 'Escriba un correo electrónico válido.', phone: 'Revise el número de teléfono o déjelo vacío.', failed: 'No se pudo enviar su solicitud. Inténtelo de nuevo.',
|
|
132
|
+
},
|
|
133
|
+
mail: {
|
|
134
|
+
receivedSubject: 'Recibimos su solicitud', received: 'Recibimos su solicitud de evento. No estará en el calendario hasta que la confirmemos.', within: 'Normalmente respondemos en {time}.',
|
|
135
|
+
replySubject: 'Sobre su solicitud de evento', reply: 'Le escribimos sobre su solicitud:',
|
|
136
|
+
approvedSubject: 'Aprobamos su solicitud', approved: 'Aprobamos su solicitud de evento. Le escribiremos si necesitamos algo más.', approvedChange: 'Aprobamos su solicitud y haremos el cambio.',
|
|
137
|
+
declinedSubject: 'Sobre su solicitud de evento', declined: 'Lo sentimos, no podemos aceptar su solicitud de evento.', declinedChange: 'Lo sentimos, no podemos hacer el cambio que pidió.',
|
|
138
|
+
fromTeam: 'Del equipo:', notYou: 'Si usted no envió esta solicitud, puede ignorar este correo.',
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
fr: {
|
|
142
|
+
heading: 'Demander un événement', notice: 'Nous examinons chaque demande. Elle n’est pas au calendrier tant que nous ne l’avons pas confirmée.',
|
|
143
|
+
somethingNew: 'Quelque chose de nouveau', change: 'Un changement à un événement', what: 'Que prévoyez-vous ?', type: 'Type', date: 'Date', starts: 'Début', ends: 'Fin',
|
|
144
|
+
repeats: 'Se répète', once: 'Une seule fois', weekly: 'Chaque semaine', other: 'Autre chose', otherHint: 'Dites-nous comment cela se répète',
|
|
145
|
+
room: 'Salle', notSure: 'Je ne sais pas', free: '{room} : libre à cette heure-là.', taken: '{room} : pas libre à cette heure-là.', people: 'Combien de personnes ?', message: 'Autre chose à savoir ?', optional: 'Facultatif',
|
|
146
|
+
event: 'Quel événement ?', pickEvent: 'Choisissez un événement', onDate: 'Quelle date ?', onDateHint: 'Laissez vide si cela concerne toutes les dates.', whatChange: 'Que faut-il changer ?',
|
|
147
|
+
contact: 'Comment vous joindre', name: 'Votre nom', email: 'E-mail', phone: 'Téléphone', contactWhy: 'Nous l’utilisons uniquement pour répondre à votre demande.', privacy: 'Confidentialité',
|
|
148
|
+
send: 'Envoyer la demande', closed: 'Nous ne prenons pas de demandes ici pour le moment.', call: 'Appelez-nous au {phone}.',
|
|
149
|
+
sent: 'Demande envoyée', answerWithin: 'Nous répondons généralement sous {time}, par e-mail à {email}.', answerBy: 'Nous vous répondrons par e-mail à {email}.',
|
|
150
|
+
yours: 'Votre demande', notYet: 'Elle n’est pas au calendrier tant que nous ne l’avons pas confirmée.', sooner: 'Besoin d’une réponse plus tôt ? Appelez le {phone}.', another: 'Envoyer une autre demande',
|
|
151
|
+
aboutPeople: 'environ {count} personnes', changeTo: 'Changement : {title}', everyDate: 'toutes les dates',
|
|
152
|
+
errors: {
|
|
153
|
+
closed: 'Nous ne prenons pas de demandes ici pour le moment.', trap: 'Impossible d’envoyer cette demande.', busy: 'Trop de demandes. Réessayez dans une minute.',
|
|
154
|
+
full: 'Nous avons reçu beaucoup de demandes aujourd’hui. Réessayez demain.', changed: 'Cette demande a déjà été envoyée. Rechargez la page pour en envoyer une autre.',
|
|
155
|
+
title: 'Dites-nous ce que vous prévoyez.', type: 'Choisissez un type dans la liste.', start: 'Choisissez une date et une heure de début.', end: 'Choisissez une heure de fin dans les 24 heures après le début.',
|
|
156
|
+
past: 'Choisissez une heure qui n’est pas passée.', far: 'Choisissez une date dans les deux prochaines années.', repeat: 'Dites-nous comment cela se répète.', room: 'Choisissez une salle dans la liste.',
|
|
157
|
+
people: 'Vérifiez le nombre de personnes.', event: 'Choisissez un événement dans la liste.', date: 'Cet événement n’a pas lieu à cette date.', what: 'Dites-nous ce qu’il faut changer.',
|
|
158
|
+
name: 'Indiquez votre nom.', email: 'Indiquez une adresse e-mail valide.', phone: 'Vérifiez le numéro de téléphone ou laissez-le vide.', failed: 'Impossible d’envoyer votre demande. Réessayez.',
|
|
159
|
+
},
|
|
160
|
+
mail: {
|
|
161
|
+
receivedSubject: 'Nous avons reçu votre demande', received: 'Nous avons reçu votre demande d’événement. Elle n’est pas au calendrier tant que nous ne l’avons pas confirmée.', within: 'Nous répondons généralement sous {time}.',
|
|
162
|
+
replySubject: 'À propos de votre demande d’événement', reply: 'Nous vous écrivons au sujet de votre demande :',
|
|
163
|
+
approvedSubject: 'Votre demande est acceptée', approved: 'Nous avons accepté votre demande d’événement. Nous vous écrirons si nous avons besoin d’autre chose.', approvedChange: 'Nous avons accepté votre demande et ferons le changement.',
|
|
164
|
+
declinedSubject: 'À propos de votre demande d’événement', declined: 'Nous sommes désolés, nous ne pouvons pas accepter votre demande d’événement.', declinedChange: 'Nous sommes désolés, nous ne pouvons pas faire le changement demandé.',
|
|
165
|
+
fromTeam: 'De l’équipe :', notYou: 'Si vous n’avez pas envoyé cette demande, ignorez cet e-mail.',
|
|
166
|
+
},
|
|
167
|
+
},
|
|
168
|
+
de: {
|
|
169
|
+
heading: 'Veranstaltung anfragen', notice: 'Wir prüfen jede Anfrage. Sie steht erst im Kalender, wenn wir sie bestätigen.',
|
|
170
|
+
somethingNew: 'Etwas Neues', change: 'Änderung einer Veranstaltung', what: 'Was planen Sie?', type: 'Art', date: 'Datum', starts: 'Beginn', ends: 'Ende',
|
|
171
|
+
repeats: 'Wiederholung', once: 'Nur einmal', weekly: 'Jede Woche', other: 'Etwas anderes', otherHint: 'Sagen Sie uns, wie es sich wiederholt',
|
|
172
|
+
room: 'Raum', notSure: 'Weiß nicht', free: '{room}: zu dieser Zeit frei.', taken: '{room}: zu dieser Zeit nicht frei.', people: 'Wie viele Personen?', message: 'Sollten wir noch etwas wissen?', optional: 'Optional',
|
|
173
|
+
event: 'Welche Veranstaltung?', pickEvent: 'Veranstaltung wählen', onDate: 'Welches Datum?', onDateHint: 'Leer lassen, wenn es um alle Termine geht.', whatChange: 'Was soll sich ändern?',
|
|
174
|
+
contact: 'So erreichen wir Sie', name: 'Ihr Name', email: 'E-Mail', phone: 'Telefon', contactWhy: 'Wir verwenden dies nur, um Ihre Anfrage zu beantworten.', privacy: 'Datenschutz',
|
|
175
|
+
send: 'Anfrage senden', closed: 'Wir nehmen hier gerade keine Anfragen an.', call: 'Rufen Sie uns an: {phone}.',
|
|
176
|
+
sent: 'Anfrage gesendet', answerWithin: 'Wir antworten normalerweise innerhalb von {time}, per E-Mail an {email}.', answerBy: 'Wir antworten per E-Mail an {email}.',
|
|
177
|
+
yours: 'Ihre Anfrage', notYet: 'Sie steht erst im Kalender, wenn wir sie bestätigen.', sooner: 'Eilt es? Rufen Sie {phone} an.', another: 'Weitere Anfrage senden',
|
|
178
|
+
aboutPeople: 'etwa {count} Personen', changeTo: 'Änderung: {title}', everyDate: 'alle Termine',
|
|
179
|
+
errors: {
|
|
180
|
+
closed: 'Wir nehmen hier gerade keine Anfragen an.', trap: 'Diese Anfrage konnte nicht gesendet werden.', busy: 'Zu viele Anfragen. Versuchen Sie es in einer Minute erneut.',
|
|
181
|
+
full: 'Heute sind viele Anfragen eingegangen. Bitte versuchen Sie es morgen erneut.', changed: 'Diese Anfrage wurde schon gesendet. Laden Sie die Seite neu, um eine neue zu senden.',
|
|
182
|
+
title: 'Sagen Sie uns, was Sie planen.', type: 'Wählen Sie eine Art aus der Liste.', start: 'Wählen Sie ein Datum und eine Beginnzeit.', end: 'Wählen Sie ein Ende innerhalb eines Tages nach dem Beginn.',
|
|
183
|
+
past: 'Wählen Sie eine Zeit, die noch nicht vorbei ist.', far: 'Wählen Sie ein Datum in den nächsten zwei Jahren.', repeat: 'Sagen Sie uns, wie es sich wiederholt.', room: 'Wählen Sie einen Raum aus der Liste.',
|
|
184
|
+
people: 'Prüfen Sie die Zahl der Personen.', event: 'Wählen Sie eine Veranstaltung aus der Liste.', date: 'An diesem Datum findet die Veranstaltung nicht statt.', what: 'Sagen Sie uns, was sich ändern soll.',
|
|
185
|
+
name: 'Geben Sie Ihren Namen an.', email: 'Geben Sie eine gültige E-Mail-Adresse an.', phone: 'Prüfen Sie die Telefonnummer oder lassen Sie sie leer.', failed: 'Ihre Anfrage konnte nicht gesendet werden. Bitte versuchen Sie es erneut.',
|
|
186
|
+
},
|
|
187
|
+
mail: {
|
|
188
|
+
receivedSubject: 'Wir haben Ihre Anfrage erhalten', received: 'Wir haben Ihre Veranstaltungsanfrage erhalten. Sie steht erst im Kalender, wenn wir sie bestätigen.', within: 'Wir antworten normalerweise innerhalb von {time}.',
|
|
189
|
+
replySubject: 'Zu Ihrer Veranstaltungsanfrage', reply: 'Wir schreiben Ihnen zu Ihrer Anfrage:',
|
|
190
|
+
approvedSubject: 'Ihre Anfrage wurde angenommen', approved: 'Wir haben Ihre Veranstaltungsanfrage angenommen. Wir melden uns, falls wir noch etwas brauchen.', approvedChange: 'Wir haben Ihre Anfrage angenommen und nehmen die Änderung vor.',
|
|
191
|
+
declinedSubject: 'Zu Ihrer Veranstaltungsanfrage', declined: 'Es tut uns leid, wir können Ihre Veranstaltungsanfrage nicht annehmen.', declinedChange: 'Es tut uns leid, wir können die gewünschte Änderung nicht vornehmen.',
|
|
192
|
+
fromTeam: 'Vom Team:', notYou: 'Wenn Sie diese Anfrage nicht gesendet haben, ignorieren Sie diese E-Mail.',
|
|
193
|
+
},
|
|
194
|
+
},
|
|
195
|
+
pt: {
|
|
196
|
+
heading: 'Solicitar um evento', notice: 'Analisamos cada solicitação. Ela não entra no calendário até que a confirmemos.',
|
|
197
|
+
somethingNew: 'Algo novo', change: 'Uma mudança em um evento', what: 'O que você está planejando?', type: 'Tipo', date: 'Data', starts: 'Começa', ends: 'Termina',
|
|
198
|
+
repeats: 'Repete', once: 'Só uma vez', weekly: 'Toda semana', other: 'Outra coisa', otherHint: 'Conte como se repete',
|
|
199
|
+
room: 'Sala', notSure: 'Não sei', free: '{room}: livre nesse horário.', taken: '{room}: não está livre nesse horário.', people: 'Quantas pessoas?', message: 'Algo mais que devemos saber?', optional: 'Opcional',
|
|
200
|
+
event: 'Qual evento?', pickEvent: 'Escolha um evento', onDate: 'Qual data?', onDateHint: 'Deixe vazio se for sobre todas as datas.', whatChange: 'O que deve mudar?',
|
|
201
|
+
contact: 'Como falamos com você', name: 'Seu nome', email: 'E-mail', phone: 'Telefone', contactWhy: 'Usamos isso só para responder à sua solicitação.', privacy: 'Privacidade',
|
|
202
|
+
send: 'Enviar solicitação', closed: 'No momento não estamos recebendo solicitações aqui.', call: 'Ligue para {phone}.',
|
|
203
|
+
sent: 'Solicitação enviada', answerWithin: 'Normalmente respondemos em {time}, por e-mail para {email}.', answerBy: 'Responderemos por e-mail para {email}.',
|
|
204
|
+
yours: 'Sua solicitação', notYet: 'Ela não entra no calendário até que a confirmemos.', sooner: 'Precisa antes? Ligue para {phone}.', another: 'Enviar outra solicitação',
|
|
205
|
+
aboutPeople: 'cerca de {count} pessoas', changeTo: 'Mudança em {title}', everyDate: 'todas as datas',
|
|
206
|
+
errors: {
|
|
207
|
+
closed: 'No momento não estamos recebendo solicitações aqui.', trap: 'Não foi possível enviar esta solicitação.', busy: 'Muitas solicitações. Tente de novo em um minuto.',
|
|
208
|
+
full: 'Recebemos muitas solicitações hoje. Tente de novo amanhã.', changed: 'Esta solicitação já foi enviada. Recarregue a página para enviar outra.',
|
|
209
|
+
title: 'Conte o que você está planejando.', type: 'Escolha um tipo da lista.', start: 'Escolha uma data e um horário de início.', end: 'Escolha um horário de término até um dia depois do início.',
|
|
210
|
+
past: 'Escolha um horário que ainda não passou.', far: 'Escolha uma data nos próximos dois anos.', repeat: 'Conte como se repete.', room: 'Escolha uma sala da lista.',
|
|
211
|
+
people: 'Confira o número de pessoas.', event: 'Escolha um evento da lista.', date: 'Esse evento não acontece nessa data.', what: 'Conte o que deve mudar.',
|
|
212
|
+
name: 'Informe seu nome.', email: 'Informe um e-mail válido.', phone: 'Confira o telefone ou deixe-o vazio.', failed: 'Não foi possível enviar sua solicitação. Tente de novo.',
|
|
213
|
+
},
|
|
214
|
+
mail: {
|
|
215
|
+
receivedSubject: 'Recebemos sua solicitação', received: 'Recebemos sua solicitação de evento. Ela não entra no calendário até que a confirmemos.', within: 'Normalmente respondemos em {time}.',
|
|
216
|
+
replySubject: 'Sobre sua solicitação de evento', reply: 'Escrevemos sobre sua solicitação:',
|
|
217
|
+
approvedSubject: 'Sua solicitação foi aprovada', approved: 'Aprovamos sua solicitação de evento. Entraremos em contato se precisarmos de mais alguma coisa.', approvedChange: 'Aprovamos sua solicitação e faremos a mudança.',
|
|
218
|
+
declinedSubject: 'Sobre sua solicitação de evento', declined: 'Sentimos muito, não podemos aceitar sua solicitação de evento.', declinedChange: 'Sentimos muito, não podemos fazer a mudança que você pediu.',
|
|
219
|
+
fromTeam: 'Da equipe:', notYou: 'Se você não enviou esta solicitação, ignore este e-mail.',
|
|
220
|
+
},
|
|
221
|
+
},
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
export const requestStrings = (locale: string | null | undefined): RequestStrings =>
|
|
225
|
+
STRINGS[(locale || 'en').toLowerCase().split('-')[0]] ?? STRINGS.en;
|
|
226
|
+
|
|
227
|
+
// The staff alert and its push, in the site's main language.
|
|
228
|
+
export interface RequestStaffWords {
|
|
229
|
+
subjectNew: string; subjectChange: string; subjectMove: string;
|
|
230
|
+
intro: Record<'app' | 'web' | 'member', string>;
|
|
231
|
+
when: string; repeats: string; rooms: string; roomsUnsure: string; people: string; type: string; from: string; language: string;
|
|
232
|
+
message: string; whatChange: string; date: string; everyDate: string; open: string; why: string;
|
|
233
|
+
pushTitle: string; pushBody: string;
|
|
234
|
+
}
|
|
235
|
+
const STAFF: Record<string, RequestStaffWords> = {
|
|
236
|
+
en: { subjectNew: 'New event request: {title}', subjectChange: 'Change requested: {title}', subjectMove: 'Date move to review: {title}',
|
|
237
|
+
intro: { app: 'Sent from the app.', web: 'Sent from the website.', member: 'Sent by a host from My events.' },
|
|
238
|
+
when: 'When', repeats: 'Repeats', rooms: 'Rooms', roomsUnsure: 'Not sure', people: 'People', type: 'Kind', from: 'From', language: 'Language',
|
|
239
|
+
message: 'Their message', whatChange: 'What should change', date: 'Date', everyDate: 'Every date', open: 'Review requests',
|
|
240
|
+
why: 'You get these because you’re on the list for new event requests.', pushTitle: 'New event request', pushBody: 'For {when}. Review it in the admin.' },
|
|
241
|
+
es: { subjectNew: 'Nueva solicitud de evento: {title}', subjectChange: 'Cambio solicitado: {title}', subjectMove: 'Cambio de fecha por revisar: {title}',
|
|
242
|
+
intro: { app: 'Enviada desde la app.', web: 'Enviada desde el sitio web.', member: 'Enviada por un anfitrión desde Mis eventos.' },
|
|
243
|
+
when: 'Cuándo', repeats: 'Se repite', rooms: 'Salones', roomsUnsure: 'No está seguro', people: 'Personas', type: 'Tipo', from: 'De', language: 'Idioma',
|
|
244
|
+
message: 'Su mensaje', whatChange: 'Qué debe cambiar', date: 'Fecha', everyDate: 'Todas las fechas', open: 'Revisar solicitudes',
|
|
245
|
+
why: 'Recibe estos correos porque está en la lista de nuevas solicitudes de eventos.', pushTitle: 'Nueva solicitud de evento', pushBody: 'Para el {when}. Revísela en el panel.' },
|
|
246
|
+
fr: { subjectNew: 'Nouvelle demande d’événement : {title}', subjectChange: 'Changement demandé : {title}', subjectMove: 'Changement de date à examiner : {title}',
|
|
247
|
+
intro: { app: 'Envoyée depuis l’app.', web: 'Envoyée depuis le site.', member: 'Envoyée par un responsable depuis Mes événements.' },
|
|
248
|
+
when: 'Quand', repeats: 'Se répète', rooms: 'Salles', roomsUnsure: 'Ne sait pas', people: 'Personnes', type: 'Type', from: 'De', language: 'Langue',
|
|
249
|
+
message: 'Son message', whatChange: 'Ce qu’il faut changer', date: 'Date', everyDate: 'Toutes les dates', open: 'Voir les demandes',
|
|
250
|
+
why: 'Vous recevez ces e-mails parce que vous êtes sur la liste des nouvelles demandes d’événements.', pushTitle: 'Nouvelle demande d’événement', pushBody: 'Pour le {when}. À examiner dans l’administration.' },
|
|
251
|
+
de: { subjectNew: 'Neue Veranstaltungsanfrage: {title}', subjectChange: 'Änderung angefragt: {title}', subjectMove: 'Terminverschiebung zu prüfen: {title}',
|
|
252
|
+
intro: { app: 'Über die App gesendet.', web: 'Über die Website gesendet.', member: 'Von einer Leitung über Meine Veranstaltungen gesendet.' },
|
|
253
|
+
when: 'Wann', repeats: 'Wiederholung', rooms: 'Räume', roomsUnsure: 'Weiß nicht', people: 'Personen', type: 'Art', from: 'Von', language: 'Sprache',
|
|
254
|
+
message: 'Nachricht', whatChange: 'Was sich ändern soll', date: 'Datum', everyDate: 'Alle Termine', open: 'Anfragen ansehen',
|
|
255
|
+
why: 'Sie erhalten diese E-Mails, weil Sie auf der Liste für neue Veranstaltungsanfragen stehen.', pushTitle: 'Neue Veranstaltungsanfrage', pushBody: 'Für {when}. Bitte in der Verwaltung prüfen.' },
|
|
256
|
+
pt: { subjectNew: 'Nova solicitação de evento: {title}', subjectChange: 'Mudança solicitada: {title}', subjectMove: 'Mudança de data para revisar: {title}',
|
|
257
|
+
intro: { app: 'Enviada pelo app.', web: 'Enviada pelo site.', member: 'Enviada por um anfitrião em Meus eventos.' },
|
|
258
|
+
when: 'Quando', repeats: 'Repete', rooms: 'Salas', roomsUnsure: 'Não sabe', people: 'Pessoas', type: 'Tipo', from: 'De', language: 'Idioma',
|
|
259
|
+
message: 'Mensagem', whatChange: 'O que deve mudar', date: 'Data', everyDate: 'Todas as datas', open: 'Ver solicitações',
|
|
260
|
+
why: 'Você recebe estes e-mails porque está na lista de novas solicitações de eventos.', pushTitle: 'Nova solicitação de evento', pushBody: 'Para {when}. Revise no painel.' },
|
|
261
|
+
};
|
|
262
|
+
export const requestStaffWords = (locale: string | null | undefined): RequestStaffWords =>
|
|
263
|
+
STAFF[(locale || 'en').toLowerCase().split('-')[0]] ?? STAFF.en;
|
|
@@ -6,12 +6,12 @@
|
|
|
6
6
|
// Cancelling one date of a series applies at once too, without touching any
|
|
7
7
|
// staged edits.
|
|
8
8
|
import type { ContentRepository } from '../repo/types';
|
|
9
|
-
import type { CalEvent, MovedDate } from '../schema';
|
|
9
|
+
import type { CalEvent, MovedDate, SeriesSegment } from '../schema';
|
|
10
10
|
import { EventLocation, slugify } from '../schema';
|
|
11
11
|
import { defaultPlaceLocation } from '../maps/places';
|
|
12
12
|
import { ApiError } from '../api';
|
|
13
13
|
import { applyEventUpdate, eventSideEffects, flushEventHooks, saveEvent, type SaveHooks } from '../content-service';
|
|
14
|
-
import { addWallDays, addWallMinutes, findOccurrence, formatWall, parseWall, wallMinutesBetween } from './events';
|
|
14
|
+
import { addWallDays, addWallMinutes, findOccurrence, formatWall, parseWall, wallMinutesBetween, partOf, partMinutes } from './events';
|
|
15
15
|
import { moveOccurrence, moveOccurrenceBack, type MoveActor } from './moves';
|
|
16
16
|
import type { BusySlot } from '../booking/holds';
|
|
17
17
|
import { rruleMap, WEEKDAY_CODES, weekdayOf } from './rrule';
|
|
@@ -105,6 +105,8 @@ const dayOf = (wall: string) => {
|
|
|
105
105
|
|
|
106
106
|
export type MoveUndo =
|
|
107
107
|
| { kind: 'series'; id: string; days: number; minutes?: number }
|
|
108
|
+
// this and following dates (lib/calendar/segments.ts): the segments as they were, and where the change took over
|
|
109
|
+
| { kind: 'following'; id: string; from: string; previous: SeriesSegment[] }
|
|
108
110
|
// one date moved (previous: where it was moved before, null = not moved)
|
|
109
111
|
| { kind: 'date'; id: string; key: string; previous: Pick<MovedDate, 'start' | 'end' | 'roomIds' | 'location' | 'note'> | null }
|
|
110
112
|
// undo tokens from before moved dates: the date was split off into a copy
|
|
@@ -126,10 +128,16 @@ export async function moveSeries(repo: ContentRepository, id: string, days: numb
|
|
|
126
128
|
...(e.rrule ? { rrule: shiftRrule(e.rrule, dayShift, start), exdates: e.exdates.map((x) => shiftBy(x, total)) } : {}),
|
|
127
129
|
}, hooks);
|
|
128
130
|
// cancelled dates move with the series, like skipped ones; a moved date
|
|
129
|
-
// keeps its new time and follows under its new key
|
|
130
|
-
|
|
131
|
+
// keeps its new time and follows under its new key; a change from a date on
|
|
132
|
+
// (segments) moves as a whole: its change point, first date and rule
|
|
133
|
+
if (e.rrule && (e.cancelledDates.length || Object.keys(e.movedDates).length || e.segments.length)) {
|
|
131
134
|
const moved = Object.fromEntries(Object.entries(e.movedDates).map(([k, m]) => [shiftBy(k, total), m]));
|
|
132
|
-
|
|
135
|
+
const segments = e.segments.map((g) => {
|
|
136
|
+
const gs = shiftBy(g.start, total);
|
|
137
|
+
const shift = ((a, b) => (a && b ? Math.round(wallMinutesBetween(a, b) / 1440) : days))(dayOf(g.start), dayOf(gs));
|
|
138
|
+
return { ...g, from: shiftBy(g.from, total), start: gs, end: shiftBy(g.end, total), rrule: shiftRrule(g.rrule, shift, gs) };
|
|
139
|
+
});
|
|
140
|
+
await repo.setEventDates(id, { movedDates: moved, cancelledDates: e.cancelledDates.map((x) => shiftBy(x, total)), segments });
|
|
133
141
|
await flushEventHooks(repo, hooks);
|
|
134
142
|
event = (await repo.getEvent(id))!;
|
|
135
143
|
}
|
|
@@ -149,21 +157,47 @@ export async function moveDateBy(repo: ContentRepository, actor: MoveActor, id:
|
|
|
149
157
|
}
|
|
150
158
|
if (!Number.isInteger(minutes) || Math.abs(minutes) > 1440 * 3660) throw new ApiError(400, 'Pick a day to move it to.');
|
|
151
159
|
const prior = e.movedDates[key];
|
|
152
|
-
const es = parseWall(e.start), ee = parseWall(e.end) ?? es;
|
|
153
160
|
const k = parseWall(key);
|
|
154
|
-
|
|
161
|
+
const at = k && findOccurrence({ ...e, movedDates: {} }, key, await repo.getSettings());
|
|
162
|
+
if (!k || !at) throw new ApiError(400, 'That date isn’t part of this event.');
|
|
163
|
+
// its length is its part's (a change from a date on may have its own)
|
|
155
164
|
const startWall = prior?.start ?? key;
|
|
156
|
-
const endWall = prior?.end ?? formatWall(addWallMinutes(k,
|
|
165
|
+
const endWall = prior?.end ?? formatWall(addWallMinutes(k, partMinutes(partOf(e, at))));
|
|
157
166
|
const r = await moveOccurrence(repo, actor, id, key, { start: shiftBy(startWall, minutes), end: shiftBy(endWall, minutes) }, hooks);
|
|
158
167
|
const previous = prior ? { start: prior.start, end: prior.end, roomIds: prior.roomIds, location: prior.location, note: prior.note } : null;
|
|
159
168
|
return { event: r.event, clashes: r.clashes, undo: { kind: 'date', id, key, previous } };
|
|
160
169
|
}
|
|
161
170
|
|
|
171
|
+
// Move one date of a repeating event and every date after it by `minutes`
|
|
172
|
+
// (a drag on the Schedule, "This and following dates"): a change from that
|
|
173
|
+
// date on (lib/calendar/segments.ts) whose rule follows the new weekday or
|
|
174
|
+
// day, keeping each date's length. The dates before it stay where they are.
|
|
175
|
+
export async function moveFollowingBy(repo: ContentRepository, actor: MoveActor, id: string, key: string, minutes: number, hooks: SaveHooks): Promise<{ event: CalEvent; undo: MoveUndo; clashes: BusySlot[] }> {
|
|
176
|
+
const e = await repo.getEvent(id);
|
|
177
|
+
if (!e) throw new ApiError(404, 'Event not found.');
|
|
178
|
+
if (!Number.isInteger(minutes) || Math.abs(minutes) > 1440 * 3660) throw new ApiError(400, 'Pick a day to move it to.');
|
|
179
|
+
const { changeFromDate, changeFromDefaults } = await import('./segments');
|
|
180
|
+
const from = await changeFromDefaults(repo, id, key);
|
|
181
|
+
if (!from) throw new ApiError(400, 'That date isn’t part of this event.');
|
|
182
|
+
// a moved date was dragged from where it is now
|
|
183
|
+
const moved = e.movedDates[key];
|
|
184
|
+
const r = await changeFromDate(repo, actor, id, key, { start: shiftBy(moved?.start ?? from.start, minutes), end: shiftBy(moved?.end ?? from.end, minutes) }, hooks);
|
|
185
|
+
// undo puts the segments back, from the earliest one that changed
|
|
186
|
+
const same = (a: SeriesSegment, list: SeriesSegment[]) => list.some((x) => JSON.stringify(x) === JSON.stringify(a));
|
|
187
|
+
const changed = [...r.event.segments.filter((g) => !same(g, e.segments)), ...e.segments.filter((g) => !same(g, r.event.segments))].map((g) => g.from).sort();
|
|
188
|
+
return { event: r.event, clashes: r.clashes, undo: { kind: 'following', id, from: changed[0] ?? key, previous: e.segments } };
|
|
189
|
+
}
|
|
190
|
+
|
|
162
191
|
export async function undoMove(repo: ContentRepository, actor: MoveActor, undo: MoveUndo, hooks: SaveHooks): Promise<void> {
|
|
163
192
|
if (undo.kind === 'series') {
|
|
164
193
|
await moveSeries(repo, undo.id, undo.days, hooks, undo.minutes ?? 0);
|
|
165
194
|
return;
|
|
166
195
|
}
|
|
196
|
+
if (undo.kind === 'following') {
|
|
197
|
+
const { restoreSegments } = await import('./segments');
|
|
198
|
+
await restoreSegments(repo, actor, undo.id, undo.from, undo.previous, hooks);
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
167
201
|
if (undo.kind === 'date') {
|
|
168
202
|
if (undo.previous) await moveOccurrence(repo, actor, undo.id, undo.key, { ...undo.previous, roomIds: undo.previous.roomIds ?? [], location: undo.previous.location ?? null }, hooks);
|
|
169
203
|
else await moveOccurrenceBack(repo, actor, undo.id, undo.key, hooks);
|