@acms-subir/core 0.3.43 → 0.3.45

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.
@@ -0,0 +1,7 @@
1
+ import { roomFree } from '../../../../lib/mobile/calendar';
2
+ import { privateRoute } from '../../../../lib/mobile/route';
3
+ import { methodNotAllowed } from '../../../../lib/mobile/http';
4
+ export { preflight as OPTIONS } from '../../../../lib/mobile/http';
5
+ // never cached: a room taken a minute ago must not read as free
6
+ export const GET = privateRoute(roomFree);
7
+ export const ALL = () => methodNotAllowed('GET, OPTIONS');
@@ -36,6 +36,7 @@ export interface ServerUser {
36
36
 
37
37
  export type { CalEvent, Occurrence };
38
38
  export type { EventRequestInput } from '../lib/calendar/requests';
39
+ export type { EventClash, BusySlot, Hold } from '../lib/booking/holds';
39
40
 
40
41
  export interface ListEventsOptions {
41
42
  /** only published events (default: every event except archived ones) */
@@ -171,6 +172,42 @@ export async function restoreOccurrence(repo: Repo, eventId: string, occurrenceK
171
172
  return restore(internal(repo), eventId, occurrenceKey, { url: context.url, locals: context.locals });
172
173
  }
173
174
 
175
+ // ---- rooms -----------------------------------------------------------------
176
+ // Events hold rooms (CalEvent.roomHold) while they happen. These find where
177
+ // two things want the same room. They don't check permissions, and their
178
+ // results carry names: show them to staff only.
179
+
180
+ /**
181
+ * Other events holding the same rooms as this one at overlapping times, setup
182
+ * and cleanup included, soonest first. Pass an event id, or a CalEvent with
183
+ * unsaved values (it's checked as if published). Looks `horizonDays` ahead
184
+ * (default 366) from `now`. Clashes are warnings: both events keep the room.
185
+ * Throws an Error with `status` 404 for an unknown event id.
186
+ */
187
+ export async function eventRoomClashes(repo: Repo, event: string | CalEvent, opts: { horizonDays?: number; now?: Date | number } = {}): Promise<import('../lib/booking/holds').EventClash[]> {
188
+ const r = internal(repo);
189
+ const e = typeof event === 'string' ? await r.getEvent(event) : event;
190
+ if (!e) throw Object.assign(new Error('Event not found.'), { status: 404 });
191
+ const { eventClashes } = await import('../lib/booking/holds');
192
+ return (await eventClashes(r, await r.getSettings(), e, { horizonDays: opts.horizonDays, now: opts.now === undefined ? undefined : +opts.now })).clashes;
193
+ }
194
+
195
+ /**
196
+ * Everything that takes these rooms (Resource ids) in [from, to), soonest
197
+ * first: events holding them (setup and cleanup included), pending and
198
+ * confirmed bookings, staff blocks and the rooms' busy calendars. `ignore`
199
+ * leaves out one event, or one date of it by occurrence key, such as the date
200
+ * you're about to move. An empty list means they're all free.
201
+ */
202
+ export async function roomBusy(repo: Repo, q: { roomIds: string[]; from: Date | number; to: Date | number; ignore?: { eventId: string; key?: string } }): Promise<import('../lib/booking/holds').BusySlot[]> {
203
+ const from = +q.from, to = +q.to;
204
+ if (!Number.isFinite(from) || !Number.isFinite(to) || to <= from) throw new RangeError('roomBusy needs a "to" after "from".');
205
+ if (to - from > MAX_WINDOW) throw new RangeError('roomBusy windows are at most 400 days.');
206
+ const r = internal(repo);
207
+ const { roomBusy: busy } = await import('../lib/booking/holds');
208
+ return busy(r, await r.getSettings(), { resourceIds: q.roomIds, from, to, ignore: q.ignore });
209
+ }
210
+
174
211
  // ---- members' event requests ------------------------------------------
175
212
  // The rules behind the built-in "My events" pages (acms({ members })), for a
176
213
  // site that builds its own. Each takes the person acting (currentUser()) and
@@ -1392,6 +1392,9 @@ em.manual { color: var(--amber); }
1392
1392
  .res-grid > div > .card + .card, .res-side > .card + .card { margin-top: 16px; }
1393
1393
  .res-side { display: flex; flex-direction: column; gap: 0; }
1394
1394
  .res-side > .btn { margin-top: 14px; align-self: flex-start; }
1395
+ .res-color { display: flex; align-items: center; gap: 12px; margin-top: 4px; }
1396
+ .res-color input[type='color'] { width: 44px; height: 30px; padding: 2px; border: 1px solid var(--line); border-radius: var(--r-sm); background: var(--panel); cursor: pointer; }
1397
+ .res-color .check-row { margin: 0; }
1395
1398
  .card-pad { padding: 16px 18px; }
1396
1399
  .card-head { display: flex; justify-content: space-between; align-items: baseline; gap: 10px; margin-bottom: 12px; }
1397
1400
  .card-head b { font-size: 14.5px; }
@@ -1039,6 +1039,8 @@
1039
1039
  .rh-conflict > b { display: block; margin-bottom: 4px; }
1040
1040
  .rh-bk { display: flex; flex-direction: column; padding: 5px 0; border-top: 1px solid color-mix(in srgb, var(--amber) 25%, transparent); }
1041
1041
  .rh-bk small, .rh-more { color: var(--text-2); font-size: 11.5px; }
1042
+ .rh-clash .rh-bk a { color: inherit; font-weight: 600; }
1043
+ .rh-clash > .rh-more:last-child { display: block; margin-top: 6px; }
1042
1044
  .rh-acts { display: flex; gap: 6px; flex-wrap: wrap; margin-top: 8px; }
1043
1045
  .rh-acts.end { justify-content: flex-end; }
1044
1046
  .rh-cancel textarea, .rh-msg { width: 100%; margin-top: 8px; padding: 7px 9px; border: 1px solid var(--line-strong); border-radius: 8px; background: var(--bg); color: var(--text); font: inherit; font-size: 12.5px; resize: vertical; }