@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.
- package/CHANGELOG.md +32 -0
- package/cli/main.mjs +3 -2
- package/cli/migrations.generated.mjs +11 -0
- package/docs/calendar.md +112 -0
- package/docs/mobile.md +1 -0
- package/docs/server.md +47 -2
- package/integration.mjs +2 -0
- package/package.json +1 -1
- package/src/actions/index.ts +14 -4
- package/src/components/calendar/QuickAdd.tsx +1 -1
- package/src/components/calendar/RoomHolds.tsx +23 -5
- package/src/lib/booking/holds.ts +98 -2
- package/src/lib/booking/service.ts +2 -1
- package/src/lib/calendar/agent.ts +2 -1
- package/src/lib/calendar/api.ts +16 -3
- package/src/lib/calendar/data.ts +12 -1
- package/src/lib/calendar/events.ts +4 -2
- package/src/lib/calendar/hooks.ts +2 -2
- package/src/lib/calendar/schedule.ts +20 -5
- package/src/lib/editor/schedule.ts +3 -0
- package/src/lib/mobile/calendar.ts +49 -11
- package/src/lib/mobile/openapi.ts +3 -1
- package/src/lib/mobile/schemas.ts +8 -3
- package/src/lib/mobile/search.ts +5 -6
- package/src/lib/repo/migrations.ts +11 -0
- package/src/lib/repo/sql.ts +23 -8
- package/src/lib/repo/types.ts +7 -0
- package/src/lib/schema.ts +10 -0
- package/src/lib/sync/catalog.ts +2 -0
- package/src/pages/admin/calendar/resources/[id].astro +13 -1
- package/src/pages/api/calendar/events/[id].ts +2 -2
- package/src/pages/api/calendar/events/index.ts +7 -4
- package/src/pages/api/v1/rooms/free.ts +7 -0
- package/src/server/index.ts +37 -0
- package/src/styles/admin.css +3 -0
- package/src/styles/editor.css +2 -0
|
@@ -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');
|
package/src/server/index.ts
CHANGED
|
@@ -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
|
package/src/styles/admin.css
CHANGED
|
@@ -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; }
|
package/src/styles/editor.css
CHANGED
|
@@ -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; }
|