@acms-subir/core 0.3.56 → 0.3.58
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 +24 -5
- package/docs/calendar.md +130 -2
- package/docs/server.md +33 -1
- package/package.json +1 -1
- package/src/actions/index.ts +52 -9
- package/src/components/EventPage.astro +6 -5
- package/src/components/calendar/CalendarBoard.tsx +6 -3
- package/src/components/editor/event/SingleDates.tsx +75 -15
- package/src/lib/booking/service.ts +4 -2
- package/src/lib/calendar/api.ts +29 -8
- 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 +16 -7
- 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 +2 -1
- package/src/lib/calendar/request-mail.ts +2 -1
- package/src/lib/calendar/request-page.ts +2 -1
- package/src/lib/calendar/request-service.ts +7 -7
- package/src/lib/calendar/schedule.ts +42 -8
- package/src/lib/calendar/segments.ts +522 -0
- package/src/lib/display/board.ts +6 -5
- package/src/lib/editor/schedule.ts +4 -2
- package/src/lib/mobile/calendar.ts +6 -5
- package/src/lib/mobile/schemas.ts +10 -3
- package/src/lib/mobile/search.ts +2 -1
- package/src/lib/push/service.ts +42 -10
- package/src/lib/push/types.ts +2 -0
- package/src/lib/repo/migrations.ts +13 -3
- package/src/lib/repo/sql.ts +34 -13
- package/src/lib/repo/types.ts +3 -1
- package/src/lib/schema.ts +34 -0
- package/src/lib/sections.ts +3 -2
- package/src/lib/sync/catalog.ts +1 -0
- package/src/pages/admin/calendar/events/index.astro +9 -8
- package/src/pages/admin/calendar/requests.astro +5 -5
- package/src/pages/admin/calendar/resources/[id].astro +11 -0
- package/src/pages/api/calendar/events/index.ts +2 -1
- package/src/pages/members/events/index.astro +87 -7
- package/src/server/index.ts +46 -3
- package/src/styles/editor.css +2 -0
|
@@ -106,8 +106,9 @@ export const GET: APIRoute = async ({ request, locals, url }) => {
|
|
|
106
106
|
from: { start: o.allDay ? isoWithOffset(o.moved.from.start, etz).slice(0, 10) : isoWithOffset(o.moved.from.start, etz), end: o.allDay ? isoWithOffset(o.moved.from.end - 86_400_000, etz).slice(0, 10) : isoWithOffset(o.moved.from.end, etz) },
|
|
107
107
|
note: o.moved.note, location: o.moved.location ?? null,
|
|
108
108
|
} : null,
|
|
109
|
-
// that date's rooms
|
|
109
|
+
// that date's rooms, and its own place (moved there, or from its segment; null = the event's)
|
|
110
110
|
rooms: roomsToJson(o.roomIds, resources, full),
|
|
111
|
+
location: o.location ?? null,
|
|
111
112
|
event: toJson(o.event.id),
|
|
112
113
|
};
|
|
113
114
|
}),
|
|
@@ -5,9 +5,11 @@ import { currentUser } from '../../../server';
|
|
|
5
5
|
import { coded } from '../../../lib/api';
|
|
6
6
|
import { readForm } from '../../../lib/forms/http';
|
|
7
7
|
import { MEMBERS, memberEventsPath, hostedEvents, withdrawEventRequest, dismissEventRequest, setHostedOccurrence } from '../../../lib/calendar/requests';
|
|
8
|
-
import { dismissDateRequest, moveHostedDate, moveHostedDateBack, myDateRequests, requestDateMove, withdrawDateRequest, type MoveRequestData } from '../../../lib/calendar/host-requests';
|
|
8
|
+
import { dismissDateRequest, moveHostedDate, moveHostedDateBack, myDateRequests, requestDateMove, requestFromDate, withdrawDateRequest, type FromDateRequestData, type MoveRequestData } from '../../../lib/calendar/host-requests';
|
|
9
|
+
import { changeFromDate, changeFromDefaults, type ChangeFromInput } from '../../../lib/calendar/segments';
|
|
10
|
+
import { rruleMap, WEEKDAY_CODES, weekdayOf } from '../../../lib/calendar/rrule';
|
|
9
11
|
import { eventUrl } from '../../../lib/calendar/config';
|
|
10
|
-
import { eventIsPublic, eventRoomIds, eventTimeZone, expandEvents, featuredOccurrence, findOccurrence, formatOccurrence, type Occurrence } from '../../../lib/calendar/events';
|
|
12
|
+
import { describeSeries, eventIsPublic, eventRoomIds, eventTimeZone, expandEvents, featuredOccurrence, findOccurrence, formatOccurrence, type Occurrence } from '../../../lib/calendar/events';
|
|
11
13
|
import { tzParts } from '../../../lib/calendar/ics';
|
|
12
14
|
import { describeRepeat } from '../../../lib/calendar/rrule';
|
|
13
15
|
import { requestLabel } from '../../../lib/calendar/member-form';
|
|
@@ -15,6 +17,7 @@ import { mergeEventDraft } from '../../../lib/drafts';
|
|
|
15
17
|
import { fill } from '../../../lib/live/words';
|
|
16
18
|
import { localizeDoc } from '../../../lib/translations';
|
|
17
19
|
import type { CalEvent, RequestRecord } from '../../../lib/schema';
|
|
20
|
+
import { roomName as nameIn } from '../../../lib/schema';
|
|
18
21
|
import type { MoveInput } from '../../../lib/calendar/moves';
|
|
19
22
|
import { failure, keepPrivate, memberLanguage, notFound, redirectTo, signInRedirect } from '../_http';
|
|
20
23
|
|
|
@@ -57,6 +60,25 @@ function nextDay(day: string): string {
|
|
|
57
60
|
return d.toISOString().slice(0, 10);
|
|
58
61
|
}
|
|
59
62
|
|
|
63
|
+
/**
|
|
64
|
+
* The "Change from this date on" form → ChangeFromInput: the first date and
|
|
65
|
+
* times as for a move, and a repeat choice worded around that first date
|
|
66
|
+
* ('same' keeps the rule in effect, moved to its weekday; the others end as
|
|
67
|
+
* that rule does).
|
|
68
|
+
*/
|
|
69
|
+
async function fromDateInput(form: FormData, e: CalEvent | null, key: string): Promise<ChangeFromInput> {
|
|
70
|
+
const base = moveInput(form, e);
|
|
71
|
+
const choice = String(form.get('repeat') ?? 'same');
|
|
72
|
+
if (choice === 'same' || !e) return base;
|
|
73
|
+
const date = String(form.get('date') ?? '').trim();
|
|
74
|
+
const wd = weekdayOf(date);
|
|
75
|
+
const code = WEEKDAY_CODES[wd.dow];
|
|
76
|
+
const kept = rruleMap((await changeFromDefaults(repo, e.id, key))?.rrule ?? '');
|
|
77
|
+
const ends = kept.COUNT ? `;COUNT=${kept.COUNT}` : kept.UNTIL ? `;UNTIL=${kept.UNTIL}` : '';
|
|
78
|
+
const rule = { weekly: `FREQ=WEEKLY;BYDAY=${code}`, biweekly: `FREQ=WEEKLY;INTERVAL=2;BYDAY=${code}`, monthly: `FREQ=MONTHLY;BYDAY=${wd.nth >= 5 ? -1 : wd.nth}${code}`, daily: 'FREQ=DAILY' }[choice];
|
|
79
|
+
return { ...base, rrule: choice === 'once' ? '' : rule ? rule + ends : undefined };
|
|
80
|
+
}
|
|
81
|
+
|
|
60
82
|
let error = '';
|
|
61
83
|
// a move refused because a room is taken: shown under its date, with "Send as a request"
|
|
62
84
|
let clash: { id: string; key: string; back: boolean; message: string; input: MoveInput | null } | null = null;
|
|
@@ -108,6 +130,16 @@ if (Astro.request.method === 'POST') {
|
|
|
108
130
|
clash = { id, key, back: true, message: failure(err, w, lang).message, input: null };
|
|
109
131
|
Astro.response.status = 409;
|
|
110
132
|
}
|
|
133
|
+
} else if (op === 'from-date') {
|
|
134
|
+
// staff who host apply it; other hosts send it to them
|
|
135
|
+
posted = { id, key, form };
|
|
136
|
+
const input = await fromDateInput(form, await repo.getEvent(id), key);
|
|
137
|
+
if (user.can('content.edit')) {
|
|
138
|
+
await changeFromDate(repo, user, id, key, input, hooks);
|
|
139
|
+
return redirectTo(`${home}?fromDateDone=1`);
|
|
140
|
+
}
|
|
141
|
+
await requestFromDate(repo, user, id, key, input, String(form.get('message') ?? ''), hooks);
|
|
142
|
+
return redirectTo(`${home}?fromDateSent=1`);
|
|
111
143
|
} else if (op === 'withdraw-request') {
|
|
112
144
|
await withdrawDateRequest(repo, user, String(form.get('request') ?? ''));
|
|
113
145
|
return redirectTo(`${home}?withdrawn=1`);
|
|
@@ -122,7 +154,7 @@ if (Astro.request.method === 'POST') {
|
|
|
122
154
|
}
|
|
123
155
|
}
|
|
124
156
|
|
|
125
|
-
const FLASH: [keyof typeof w.flash, string][] = (['sent', 'updated', 'changes', 'withdrawn', 'cancelled', 'restored', 'moved', 'movedBack', 'moveSent', 'fromDateSent'] as const).map((k) => [k, w.flash[k]]);
|
|
157
|
+
const FLASH: [keyof typeof w.flash, string][] = (['sent', 'updated', 'changes', 'withdrawn', 'cancelled', 'restored', 'moved', 'movedBack', 'moveSent', 'fromDateSent', 'fromDateDone'] as const).map((k) => [k, w.flash[k]]);
|
|
126
158
|
const flash = error || clash ? '' : FLASH.find(([k]) => Astro.url.searchParams.has(k))?.[1] ?? '';
|
|
127
159
|
|
|
128
160
|
const DAY = 86_400_000;
|
|
@@ -147,14 +179,16 @@ const when = (o: Pick<Occurrence, 'event' | 'start' | 'end' | 'allDay' | 'days'>
|
|
|
147
179
|
};
|
|
148
180
|
const day = (ms: number) => format({ month: 'short', day: 'numeric' }, siteTz).format(ms);
|
|
149
181
|
const meta = (...parts: string[]) => parts.filter(Boolean).join(' · ');
|
|
150
|
-
|
|
182
|
+
// the rule in effect, and a change from a date on still to come
|
|
183
|
+
const repeats = (e: CalEvent) => describeSeries(e, settings, now, lang, undefined, dateLocale);
|
|
151
184
|
const pad = (n: number) => String(n).padStart(2, '0');
|
|
152
185
|
/** an instant as the move form's date and time, in the event's zone */
|
|
153
186
|
const wallParts = (e: CalEvent, ms: number) => {
|
|
154
187
|
const p = tzParts(ms, e.allDay ? siteTz : eventTimeZone(e, settings));
|
|
155
188
|
return { date: `${p.y}-${pad(p.m)}-${pad(p.d)}`, time: `${pad(p.hh)}:${pad(p.mm)}` };
|
|
156
189
|
};
|
|
157
|
-
|
|
190
|
+
// the rooms in the page's language (their main names without one)
|
|
191
|
+
const roomName = new Map(resources.map((r) => [r.id, nameIn(r, lang)]));
|
|
158
192
|
const requestable = resources.filter((r) => r.requestable).map((r) => r.id);
|
|
159
193
|
/** the rooms a host may pick for a date: the event's own (and its dates'), and the requestable ones */
|
|
160
194
|
const roomChoices = (e: CalEvent) => [...new Set([...eventRoomIds(e), ...requestable])].filter((id) => roomName.has(id)).map((id) => ({ id, name: roomName.get(id)! }));
|
|
@@ -169,12 +203,21 @@ const requestTarget = (e: CalEvent, r: RequestRecord) => {
|
|
|
169
203
|
// …worded for its card: "Thu, Jan 10 · 6:00 – 7:00 PM → Wed, Jan 9 · 5:00 – 6:00 PM" while it waits, else the date as it is
|
|
170
204
|
const requestWhen = (r: RequestRecord) => {
|
|
171
205
|
const e = byId.get(r.eventId);
|
|
206
|
+
if (e && r.kind === 'from-date') {
|
|
207
|
+
// the date it changes from (while it's still there), then the new first date and its repeat
|
|
208
|
+
const f = r.data as FromDateRequestData;
|
|
209
|
+
const first = findOccurrence({ ...e, start: f.start, end: f.end, rrule: f.rrule, segments: [], movedDates: {}, cancelledDates: [], exdates: [] }, f.start, settings);
|
|
210
|
+
const from = r.state === 'pending' ? findOccurrence({ ...e, movedDates: {} }, r.key, settings) : null;
|
|
211
|
+
const to = [first ? when(first) : '', describeRepeat(f.rrule, f.start, undefined, lang)].filter(Boolean).join(' · ');
|
|
212
|
+
return from ? `${when(from)} → ${to}` : to;
|
|
213
|
+
}
|
|
172
214
|
const at = e && findOccurrence(e, r.key, settings);
|
|
173
215
|
if (!e || !at) return '';
|
|
174
216
|
const to = r.state === 'pending' ? requestTarget(e, r) : null;
|
|
175
217
|
return to ? `${when(at)} → ${when(to)}` : when(at);
|
|
176
218
|
};
|
|
177
219
|
const waitingMove = new Map(dateRequests.filter((r) => r.kind === 'move' && r.state === 'pending').map((r) => [`${r.eventId}|${r.key}`, r]));
|
|
220
|
+
const waitingFrom = new Set(dateRequests.filter((r) => r.kind === 'from-date' && r.state === 'pending').map((r) => `${r.eventId}|${r.key}`));
|
|
178
221
|
|
|
179
222
|
// requests: waiting ones first, newest first; shown as they'd be once approved
|
|
180
223
|
const requests = [
|
|
@@ -294,13 +337,16 @@ function moveValues(e: CalEvent, o: Occurrence) {
|
|
|
294
337
|
<ul class="pc-mem-dates">
|
|
295
338
|
{dates.map((o) => {
|
|
296
339
|
const off = e.cancelledDates.includes(o.key);
|
|
297
|
-
|
|
340
|
+
// a date cancelled with every date after it (a cancelled part) is the team's to restore
|
|
341
|
+
const open = e.rrule && !e.cancelled && o.start > now && (!o.cancelled || off);
|
|
298
342
|
const waiting = waitingMove.get(`${e.id}|${o.key}`);
|
|
299
343
|
const ownMove = o.moved && e.movedDates[o.key]?.by === user.id;
|
|
300
344
|
const clashHere = clash && clash.id === e.id && clash.key === o.key ? clash : null;
|
|
301
345
|
const target = waiting && requestTarget(e, waiting);
|
|
302
346
|
const v = moveValues(e, o);
|
|
303
|
-
const
|
|
347
|
+
const failed = Boolean(posted && posted.id === e.id && posted.key === o.key && error);
|
|
348
|
+
const reopen = failed && posted!.form.get('op') !== 'from-date';
|
|
349
|
+
const reopenFrom = failed && posted!.form.get('op') === 'from-date';
|
|
304
350
|
return (
|
|
305
351
|
<li class={o.cancelled ? 'is-cancelled' : undefined}>
|
|
306
352
|
<span>
|
|
@@ -308,6 +354,7 @@ function moveValues(e: CalEvent, o: Occurrence) {
|
|
|
308
354
|
{o.cancelled && <em>{w.cancelledDate}</em>}
|
|
309
355
|
{o.moved && !o.cancelled && <em class="is-moved" title={o.moved.note || undefined}>{fill(w.movedFrom, { when: when({ ...o, start: o.moved.from.start, end: o.moved.from.end }) })}</em>}
|
|
310
356
|
{waiting && <em class="is-pending">{(waiting.data as MoveRequestData).back ? w.moveBackRequested : fill(w.moveRequested, { when: target ? when(target) : '' })}</em>}
|
|
357
|
+
{waitingFrom.has(`${e.id}|${o.key}`) && <em class="is-pending">{fill(w.changeFromRequested, { when: day(o.start) })}</em>}
|
|
311
358
|
</span>
|
|
312
359
|
{open && (
|
|
313
360
|
<span class="pc-mem-date-acts">
|
|
@@ -377,6 +424,39 @@ function moveValues(e: CalEvent, o: Occurrence) {
|
|
|
377
424
|
</form>
|
|
378
425
|
</details>
|
|
379
426
|
)}
|
|
427
|
+
{open && !off && !clashHere && (
|
|
428
|
+
<details class="pc-mem-move" open={reopenFrom}>
|
|
429
|
+
<summary class="pc-mem-btn is-small" aria-label={fill(w.changeFromLabel, { title: ev.title, when: when(o) })}>{w.changeFrom}</summary>
|
|
430
|
+
<form method="post">
|
|
431
|
+
<input type="hidden" name="op" value="from-date" />
|
|
432
|
+
<input type="hidden" name="id" value={e.id} />
|
|
433
|
+
<input type="hidden" name="key" value={o.key} />
|
|
434
|
+
<p class="pc-mem-hint">{w.changeFromHint}</p>
|
|
435
|
+
<div class="pc-mem-row">
|
|
436
|
+
<label class="pc-bk-field"><span>{w.firstDate}</span><input type="date" name="date" required value={reopenFrom ? v.date : wallParts(e, o.start).date} /></label>
|
|
437
|
+
{!e.allDay && <label class="pc-bk-field"><span>{w.starts}</span><input type="time" name="startTime" required value={reopenFrom ? v.startTime : wallParts(e, o.start).time} /></label>}
|
|
438
|
+
{!e.allDay && <label class="pc-bk-field"><span>{w.ends}</span><input type="time" name="endTime" value={reopenFrom ? v.endTime : wallParts(e, o.end).time} /></label>}
|
|
439
|
+
</div>
|
|
440
|
+
<label class="pc-bk-field"><span>{w.form.repeats}</span>
|
|
441
|
+
<select name="repeat">
|
|
442
|
+
{([['same', w.sameRepeat], ['weekly', w.form.repeat.weekly], ['biweekly', w.form.repeat.biweekly], ['monthly', w.form.repeat.monthly], ['daily', w.form.repeat.daily], ['once', w.justThisDate]] as const)
|
|
443
|
+
.map(([value, label]) => <option value={value} selected={reopenFrom && posted!.form.get('repeat') === value}>{label}</option>)}
|
|
444
|
+
</select>
|
|
445
|
+
</label>
|
|
446
|
+
{rooms.length > 0 && (
|
|
447
|
+
<fieldset>
|
|
448
|
+
<legend>{w.rooms}</legend>
|
|
449
|
+
<input type="hidden" name="rooms" value="1" />
|
|
450
|
+
{rooms.map((r) => <label class="pc-bk-check"><input type="checkbox" name="roomIds" value={r.id} checked={(reopenFrom ? v.roomIds : o.roomIds).includes(r.id)} /> <span>{r.name}</span></label>)}
|
|
451
|
+
<p class="pc-mem-hint" style="flex-basis:100%">{w.roomsHint}</p>
|
|
452
|
+
</fieldset>
|
|
453
|
+
)}
|
|
454
|
+
<label class="pc-bk-field"><span>{w.publicNote} <em>{w.form.optional}</em></span><input name="note" maxlength="500" value={reopenFrom ? v.note : ''} /><small>{w.publicNoteHint}</small></label>
|
|
455
|
+
{!user.can('content.edit') && <label class="pc-bk-field"><span>{w.messageForTeam} <em>{w.form.optional}</em></span><textarea name="message" rows="2" maxlength="1000">{reopenFrom ? v.message : ''}</textarea></label>}
|
|
456
|
+
<div class="pc-mem-actions"><button class="pc-mem-btn is-small is-primary" type="submit">{user.can('content.edit') ? w.changeFrom.replace(/…$/, '') : w.sendForApproval}</button></div>
|
|
457
|
+
</form>
|
|
458
|
+
</details>
|
|
459
|
+
)}
|
|
380
460
|
</li>
|
|
381
461
|
);
|
|
382
462
|
})}
|
package/src/server/index.ts
CHANGED
|
@@ -256,6 +256,39 @@ export async function syncGoogleCalendar(repo: Repo, context: Pick<WriteContext,
|
|
|
256
256
|
return syncGoogle(internal(repo), context.locals);
|
|
257
257
|
}
|
|
258
258
|
|
|
259
|
+
// ---- from this date on -------------------------------------------------------
|
|
260
|
+
// A repeating event changed from one of its dates on (new times, weekday,
|
|
261
|
+
// rule, rooms or place), the dates before it kept as they were. It stays one
|
|
262
|
+
// event; the change is a segment on its row (CalEvent.segments). RSVPs,
|
|
263
|
+
// cancelled and moved dates follow to the new date nearest them within half
|
|
264
|
+
// a week (`npx acms docs calendar`). Staff and GOOGLE_SYNC only (room clashes
|
|
265
|
+
// warn); hosts send requestFromDate. All are idempotent (`changed: false`);
|
|
266
|
+
// `quiet` skips the member push, and RSVP emails still go.
|
|
267
|
+
|
|
268
|
+
const segments = () => import('../lib/calendar/segments');
|
|
269
|
+
export type { ChangeFromInput, ChangeFromResult } from '../lib/calendar/segments';
|
|
270
|
+
export type { SeriesSegment } from '../lib/schema';
|
|
271
|
+
|
|
272
|
+
/** Change the series from `fromKey` (an occurrence key) on: `input.start` is the new first date (wall-clock in the event's zone), `rrule` omitted = the rule in effect on the new weekday. */
|
|
273
|
+
export async function changeFromDate(repo: Repo, who: Who | typeof GOOGLE_SYNC, eventId: string, fromKey: string, input: import('../lib/calendar/segments').ChangeFromInput, context: WriteContext, opts: { quiet?: boolean } = {}): Promise<import('../lib/calendar/segments').ChangeFromResult> {
|
|
274
|
+
return (await segments()).changeFromDate(internal(repo), who, eventId, fromKey, input, { url: context.url, locals: context.locals }, opts);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/** Cancel every date from `fromKey` on: listed, struck through, holding no room; their RSVPs are cancelled (emailed). */
|
|
278
|
+
export async function cancelFromDate(repo: Repo, who: Who | typeof GOOGLE_SYNC, eventId: string, fromKey: string, context: WriteContext, opts: { quiet?: boolean } = {}): Promise<import('../lib/calendar/segments').ChangeFromResult> {
|
|
279
|
+
return (await segments()).cancelFromDate(internal(repo), who, eventId, fromKey, { url: context.url, locals: context.locals }, opts);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/** Undo cancelFromDate (from any date it covers). The dates are back; their RSVPs stay cancelled. */
|
|
283
|
+
export async function restoreFromDate(repo: Repo, who: Who | typeof GOOGLE_SYNC, eventId: string, fromKey: string, context: WriteContext, opts: { quiet?: boolean } = {}): Promise<import('../lib/calendar/segments').ChangeFromResult> {
|
|
284
|
+
return (await segments()).restoreFromDate(internal(repo), who, eventId, fromKey, { url: context.url, locals: context.locals }, opts);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/** Remove the change that took over at `from`: the rule before it carries on, RSVPs and dates following back. */
|
|
288
|
+
export async function revertFromDate(repo: Repo, who: Who | typeof GOOGLE_SYNC, eventId: string, from: string, context: WriteContext, opts: { quiet?: boolean } = {}): Promise<import('../lib/calendar/segments').ChangeFromResult> {
|
|
289
|
+
return (await segments()).revertFromDate(internal(repo), who, eventId, from, { url: context.url, locals: context.locals }, opts);
|
|
290
|
+
}
|
|
291
|
+
|
|
259
292
|
// ---- rooms -----------------------------------------------------------------
|
|
260
293
|
// Events hold rooms (CalEvent.roomHold) while they happen. These find where
|
|
261
294
|
// two things want the same room. They don't check permissions, and their
|
|
@@ -361,7 +394,7 @@ export async function setHostedOccurrence(repo: Repo, who: Who, eventId: string,
|
|
|
361
394
|
// that staff who can publish approve or decline; both sides are emailed.
|
|
362
395
|
|
|
363
396
|
const hostRequests = () => import('../lib/calendar/host-requests');
|
|
364
|
-
export type { HostMoveResult, MoveRequestData } from '../lib/calendar/host-requests';
|
|
397
|
+
export type { HostMoveResult, MoveRequestData, FromDateRequestData } from '../lib/calendar/host-requests';
|
|
365
398
|
|
|
366
399
|
/** A host moves one date: `{ moved }` at once, or `{ request }` when moves need approval. Errors as moveOccurrence's, plus `code` room-not-allowed (a room the event doesn't use and nobody may request). */
|
|
367
400
|
export async function moveHostedDate(repo: Repo, who: Who, eventId: string, occurrenceKey: string, input: import('../lib/calendar/moves').MoveInput, context: WriteContext): Promise<import('../lib/calendar/host-requests').HostMoveResult> {
|
|
@@ -383,6 +416,11 @@ export async function requestDateMove(repo: Repo, who: Who, eventId: string, occ
|
|
|
383
416
|
return (await hostRequests()).requestDateMove(internal(repo), who, eventId, occurrenceKey, input, message, context);
|
|
384
417
|
}
|
|
385
418
|
|
|
419
|
+
/** A host asks staff to change their event from one date on (checked like changeFromDate; rooms among the event's and the requestable ones). Staff are emailed. */
|
|
420
|
+
export async function requestFromDate(repo: Repo, who: Who, eventId: string, fromKey: string, input: Omit<import('../lib/calendar/segments').ChangeFromInput, 'location'>, message: string, context: WriteContext): Promise<import('../lib/schema').RequestRecord> {
|
|
421
|
+
return (await hostRequests()).requestFromDate(internal(repo), who, eventId, fromKey, input, message, context);
|
|
422
|
+
}
|
|
423
|
+
|
|
386
424
|
/** A host's own date requests (waiting, approved, declined and withdrawn), newest first. */
|
|
387
425
|
export async function listMyDateRequests(repo: Repo, who: Who, eventId?: string): Promise<import('../lib/schema').RequestRecord[]> {
|
|
388
426
|
if (!who) return [];
|
|
@@ -394,8 +432,13 @@ export async function withdrawDateRequest(repo: Repo, who: Who, requestId: strin
|
|
|
394
432
|
return (await hostRequests()).withdrawDateRequest(internal(repo), who, requestId);
|
|
395
433
|
}
|
|
396
434
|
|
|
397
|
-
/**
|
|
398
|
-
|
|
435
|
+
/**
|
|
436
|
+
* Staff ("Publish content"): approve a host's date request. A move runs as
|
|
437
|
+
* them (room clashes come back as warnings) and is recorded as the host's
|
|
438
|
+
* (`moved`); a change from a date on runs changeFromDate (`changed`). Pass
|
|
439
|
+
* `seen`, the request's `updatedAt` you showed.
|
|
440
|
+
*/
|
|
441
|
+
export async function approveDateRequest(repo: Repo, who: Who, requestId: string, context: WriteContext, seen?: number): Promise<{ request: import('../lib/schema').RequestRecord; moved?: import('../lib/calendar/moves').MoveResult; changed?: import('../lib/calendar/segments').ChangeFromResult }> {
|
|
399
442
|
return (await hostRequests()).approveDateRequest(internal(repo), who, requestId, context, seen);
|
|
400
443
|
}
|
|
401
444
|
|
package/src/styles/editor.css
CHANGED
|
@@ -1346,4 +1346,6 @@ html.pe-evr-lock { overflow: hidden; }
|
|
|
1346
1346
|
.pe-evd-rooms legend { font-size: 12px; color: var(--text-2); margin-bottom: 4px; padding: 0; }
|
|
1347
1347
|
.pe-evd-room { display: inline-flex; align-items: center; gap: 6px; cursor: pointer; }
|
|
1348
1348
|
.pe-evd-formacts { display: flex; gap: 8px; }
|
|
1349
|
+
.pe-evd-segs { margin-bottom: 8px; padding-bottom: 4px; border-bottom: 1px solid var(--line); }
|
|
1350
|
+
.pe-evd-hint { margin: 0; font-size: 12.5px; color: var(--text-2); }
|
|
1349
1351
|
.pe-ev-note.is-error { color: var(--red); }
|