@acms-subir/core 0.3.9 → 0.3.10
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 +33 -0
- package/cli/common.mjs +2 -2
- package/cli/main.mjs +7 -1
- package/cli/mobile-doctor.mjs +9 -0
- package/cli/monitor.mjs +189 -0
- package/docs/mobile.md +23 -2
- package/integration.mjs +35 -4
- package/package.json +1 -1
- package/src/actions/index.ts +6 -26
- package/src/components/AppAccess.tsx +117 -0
- package/src/components/MonitoringSettings.tsx +112 -0
- package/src/layouts/Admin.astro +2 -3
- package/src/lib/api.ts +2 -1
- package/src/lib/backup/content.ts +1 -1
- package/src/lib/booking/availability.ts +15 -2
- package/src/lib/booking/service.ts +53 -1
- package/src/lib/calendar/admin.ts +41 -0
- package/src/lib/editor/me.ts +4 -0
- package/src/lib/editor/openapi.ts +49 -1
- package/src/lib/editor/roles.ts +20 -0
- package/src/lib/editor/schedule.ts +340 -0
- package/src/lib/editor/schemas.ts +36 -1
- package/src/lib/editor/site-data.ts +110 -0
- package/src/lib/email/service.ts +8 -1
- package/src/lib/email/store.ts +10 -0
- package/src/lib/email/templates.ts +6 -0
- package/src/lib/forms/store.ts +5 -0
- package/src/lib/mobile/access-admin.ts +34 -0
- package/src/lib/mobile/access.ts +28 -0
- package/src/lib/mobile/calendar.ts +26 -13
- package/src/lib/mobile/content.ts +21 -1
- package/src/lib/mobile/forms.ts +77 -0
- package/src/lib/mobile/guest-store.ts +62 -0
- package/src/lib/mobile/guest.ts +160 -0
- package/src/lib/mobile/http.ts +2 -2
- package/src/lib/mobile/links.ts +69 -0
- package/src/lib/mobile/manage.ts +114 -0
- package/src/lib/mobile/openapi.ts +40 -4
- package/src/lib/mobile/route.ts +24 -2
- package/src/lib/mobile/schemas.ts +52 -1
- package/src/lib/mobile/search.ts +95 -0
- package/src/lib/mobile/site.ts +8 -2
- package/src/lib/monitoring.ts +171 -0
- package/src/lib/repo/migrations.ts +28 -0
- package/src/lib/repo/sql.ts +66 -20
- package/src/lib/repo/types.ts +19 -2
- package/src/lib/scheduler.ts +7 -0
- package/src/lib/schema.ts +35 -1
- package/src/pages/admin/calendar/resources/link.astro +2 -0
- package/src/pages/admin/mobile.astro +6 -2
- package/src/pages/admin/settings.astro +12 -0
- package/src/pages/api/health.ts +17 -0
- package/src/pages/api/mobile/access.ts +22 -0
- package/src/pages/api/monitoring.ts +22 -0
- package/src/pages/api/ops/status.ts +2 -1
- package/src/pages/api/v1/admin/bookings/[id]/[action].ts +7 -1
- package/src/pages/api/v1/admin/bookings/index.ts +10 -2
- package/src/pages/api/v1/admin/calendars/[id].ts +16 -0
- package/src/pages/api/v1/admin/calendars/index.ts +12 -0
- package/src/pages/api/v1/admin/forms/[id]/responses.ts +7 -0
- package/src/pages/api/v1/admin/forms/index.ts +6 -0
- package/src/pages/api/v1/admin/links.ts +13 -0
- package/src/pages/api/v1/admin/resources/[id]/blocks/[blockId].ts +10 -0
- package/src/pages/api/v1/admin/resources/[id]/blocks/index.ts +12 -0
- package/src/pages/api/v1/admin/resources/[id]/index.ts +17 -0
- package/src/pages/api/v1/admin/resources/[id]/links/[linkId].ts +16 -0
- package/src/pages/api/v1/admin/resources/[id]/links/index.ts +12 -0
- package/src/pages/api/v1/admin/resources/index.ts +12 -0
- package/src/pages/api/v1/admin/schedule.ts +7 -0
- package/src/pages/api/v1/bookings/manage/availability.ts +6 -0
- package/src/pages/api/v1/bookings/manage/cancel.ts +6 -0
- package/src/pages/api/v1/bookings/manage/ics.ts +6 -0
- package/src/pages/api/v1/bookings/manage/index.ts +6 -0
- package/src/pages/api/v1/bookings/manage/reschedule.ts +6 -0
- package/src/pages/api/v1/forms/[id]/index.ts +7 -0
- package/src/pages/api/v1/forms/[id]/responses.ts +6 -0
- package/src/pages/api/v1/guest/bookings.ts +6 -0
- package/src/pages/api/v1/guest/code.ts +6 -0
- package/src/pages/api/v1/guest/revoke.ts +6 -0
- package/src/pages/api/v1/guest/token.ts +6 -0
- package/src/pages/api/v1/links.ts +7 -0
- package/src/pages/api/v1/search.ts +7 -0
- package/src/pages/book/[resource]/[link].astro +3 -3
- package/src/pages/book/manage/[token].astro +11 -7
- package/src/styles/admin.css +66 -0
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { editorRoute, logWrite } from '../../../../lib/editor/http';
|
|
2
|
+
import { LinksPut, getLinksFor, putLinksFor } from '../../../../lib/editor/site-data';
|
|
3
|
+
import { methodNotAllowed } from '../../../../lib/mobile/http';
|
|
4
|
+
import { readJson } from '../../../../lib/api';
|
|
5
|
+
export { preflight as OPTIONS } from '../../../../lib/mobile/http';
|
|
6
|
+
export const GET = editorRoute(async (c) => getLinksFor(c));
|
|
7
|
+
// replaces the whole page (profile + blocks in order)
|
|
8
|
+
export const PUT = editorRoute(async (c) => {
|
|
9
|
+
const result = await putLinksFor(c, LinksPut.parse(await readJson(c.context.request, 300_000)));
|
|
10
|
+
await logWrite(c, 'links-update', 'links');
|
|
11
|
+
return result;
|
|
12
|
+
}, { write: true });
|
|
13
|
+
export const ALL = () => methodNotAllowed('GET, PUT, OPTIONS');
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { editorRoute, logWrite } from '../../../../../../../lib/editor/http';
|
|
2
|
+
import { removeBlock } from '../../../../../../../lib/editor/schedule';
|
|
3
|
+
import { methodNotAllowed } from '../../../../../../../lib/mobile/http';
|
|
4
|
+
export { preflight as OPTIONS } from '../../../../../../../lib/mobile/http';
|
|
5
|
+
export const DELETE = editorRoute(async (c) => {
|
|
6
|
+
const b = await removeBlock(c, c.context.params.id!, c.context.params.blockId!);
|
|
7
|
+
await logWrite(c, 'block-delete', `booking:${b.id}`);
|
|
8
|
+
return b;
|
|
9
|
+
}, { write: true });
|
|
10
|
+
export const ALL = () => methodNotAllowed('DELETE, OPTIONS');
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { editorRoute, logWrite } from '../../../../../../../lib/editor/http';
|
|
2
|
+
import { BlockInput, addBlock } from '../../../../../../../lib/editor/schedule';
|
|
3
|
+
import { methodNotAllowed } from '../../../../../../../lib/mobile/http';
|
|
4
|
+
import { readJson } from '../../../../../../../lib/api';
|
|
5
|
+
export { preflight as OPTIONS } from '../../../../../../../lib/mobile/http';
|
|
6
|
+
// close a room for a while (no booking can land on it)
|
|
7
|
+
export const POST = editorRoute(async (c) => {
|
|
8
|
+
const b = await addBlock(c, c.context.params.id!, BlockInput.parse(await readJson(c.context.request, 5_000)));
|
|
9
|
+
await logWrite(c, 'block-create', `booking:${b.id}`);
|
|
10
|
+
return b;
|
|
11
|
+
}, { write: true, status: 201 });
|
|
12
|
+
export const ALL = () => methodNotAllowed('POST, OPTIONS');
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { editorRoute, logWrite } from '../../../../../../lib/editor/http';
|
|
2
|
+
import { ResourcePatch, deleteResourceFor, getResourceFor, updateResourceFor } from '../../../../../../lib/editor/schedule';
|
|
3
|
+
import { methodNotAllowed } from '../../../../../../lib/mobile/http';
|
|
4
|
+
import { readJson } from '../../../../../../lib/api';
|
|
5
|
+
export { preflight as OPTIONS } from '../../../../../../lib/mobile/http';
|
|
6
|
+
export const GET = editorRoute(async (c) => getResourceFor(c, c.context.params.id!));
|
|
7
|
+
export const PATCH = editorRoute(async (c) => {
|
|
8
|
+
const r = await updateResourceFor(c, c.context.params.id!, ResourcePatch.parse(await readJson(c.context.request, 100_000)));
|
|
9
|
+
await logWrite(c, 'resource-update', `resource:${r.id}`);
|
|
10
|
+
return r;
|
|
11
|
+
}, { write: true });
|
|
12
|
+
export const DELETE = editorRoute(async (c) => {
|
|
13
|
+
const r = await deleteResourceFor(c, c.context.params.id!);
|
|
14
|
+
await logWrite(c, 'resource-delete', `resource:${r.id}`);
|
|
15
|
+
return r;
|
|
16
|
+
}, { write: true });
|
|
17
|
+
export const ALL = () => methodNotAllowed('GET, PATCH, DELETE, OPTIONS');
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { editorRoute, logWrite } from '../../../../../../../lib/editor/http';
|
|
2
|
+
import { LinkPatch, deleteLinkFor, updateLinkFor } from '../../../../../../../lib/editor/schedule';
|
|
3
|
+
import { methodNotAllowed } from '../../../../../../../lib/mobile/http';
|
|
4
|
+
import { readJson } from '../../../../../../../lib/api';
|
|
5
|
+
export { preflight as OPTIONS } from '../../../../../../../lib/mobile/http';
|
|
6
|
+
export const PATCH = editorRoute(async (c) => {
|
|
7
|
+
const l = await updateLinkFor(c, c.context.params.id!, c.context.params.linkId!, LinkPatch.parse(await readJson(c.context.request, 100_000)));
|
|
8
|
+
await logWrite(c, 'booking-link-update', `booking-link:${l.id}`);
|
|
9
|
+
return l;
|
|
10
|
+
}, { write: true });
|
|
11
|
+
export const DELETE = editorRoute(async (c) => {
|
|
12
|
+
const l = await deleteLinkFor(c, c.context.params.id!, c.context.params.linkId!);
|
|
13
|
+
await logWrite(c, 'booking-link-delete', `booking-link:${l.id}`);
|
|
14
|
+
return l;
|
|
15
|
+
}, { write: true });
|
|
16
|
+
export const ALL = () => methodNotAllowed('PATCH, DELETE, OPTIONS');
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { editorRoute, logWrite } from '../../../../../../../lib/editor/http';
|
|
2
|
+
import { LinkInput, createLinkFor } from '../../../../../../../lib/editor/schedule';
|
|
3
|
+
import { methodNotAllowed } from '../../../../../../../lib/mobile/http';
|
|
4
|
+
import { readJson } from '../../../../../../../lib/api';
|
|
5
|
+
export { preflight as OPTIONS } from '../../../../../../../lib/mobile/http';
|
|
6
|
+
// a new booking type for this room or service
|
|
7
|
+
export const POST = editorRoute(async (c) => {
|
|
8
|
+
const l = await createLinkFor(c, c.context.params.id!, LinkInput.parse(await readJson(c.context.request, 100_000)));
|
|
9
|
+
await logWrite(c, 'booking-link-create', `booking-link:${l.id}`);
|
|
10
|
+
return l;
|
|
11
|
+
}, { write: true, status: 201 });
|
|
12
|
+
export const ALL = () => methodNotAllowed('POST, OPTIONS');
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { editorRoute, logWrite } from '../../../../../lib/editor/http';
|
|
2
|
+
import { ResourceInput, createResourceFor, listResourcesFor } from '../../../../../lib/editor/schedule';
|
|
3
|
+
import { methodNotAllowed } from '../../../../../lib/mobile/http';
|
|
4
|
+
import { readJson } from '../../../../../lib/api';
|
|
5
|
+
export { preflight as OPTIONS } from '../../../../../lib/mobile/http';
|
|
6
|
+
export const GET = editorRoute(async (c) => listResourcesFor(c));
|
|
7
|
+
export const POST = editorRoute(async (c) => {
|
|
8
|
+
const r = await createResourceFor(c, ResourceInput.parse(await readJson(c.context.request, 100_000)));
|
|
9
|
+
await logWrite(c, 'resource-create', `resource:${r.id}`);
|
|
10
|
+
return r;
|
|
11
|
+
}, { write: true, status: 201 });
|
|
12
|
+
export const ALL = () => methodNotAllowed('GET, POST, OPTIONS');
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { editorRoute } from '../../../../lib/editor/http';
|
|
2
|
+
import { ScheduleQuery, schedule } from '../../../../lib/editor/schedule';
|
|
3
|
+
import { methodNotAllowed, queryParams } from '../../../../lib/mobile/http';
|
|
4
|
+
export { preflight as OPTIONS } from '../../../../lib/mobile/http';
|
|
5
|
+
// bookings, blocked time, events and subscribed calendars for a date range
|
|
6
|
+
export const GET = editorRoute(async (c) => schedule(c, ScheduleQuery.parse(queryParams(c.context.url))));
|
|
7
|
+
export const ALL = () => methodNotAllowed('GET, OPTIONS');
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { managedAvailability } from '../../../../../lib/mobile/manage';
|
|
2
|
+
import { privateRoute } from '../../../../../lib/mobile/route';
|
|
3
|
+
import { methodNotAllowed } from '../../../../../lib/mobile/http';
|
|
4
|
+
export { preflight as OPTIONS } from '../../../../../lib/mobile/http';
|
|
5
|
+
export const GET = privateRoute(managedAvailability);
|
|
6
|
+
export const ALL = () => methodNotAllowed('GET, OPTIONS');
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { cancelManaged } from '../../../../../lib/mobile/manage';
|
|
2
|
+
import { privateRoute } from '../../../../../lib/mobile/route';
|
|
3
|
+
import { methodNotAllowed } from '../../../../../lib/mobile/http';
|
|
4
|
+
export { preflight as OPTIONS } from '../../../../../lib/mobile/http';
|
|
5
|
+
export const POST = privateRoute(cancelManaged, { write: true });
|
|
6
|
+
export const ALL = () => methodNotAllowed('POST, OPTIONS');
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { managedIcs } from '../../../../../lib/mobile/manage';
|
|
2
|
+
import { privateRoute } from '../../../../../lib/mobile/route';
|
|
3
|
+
import { methodNotAllowed } from '../../../../../lib/mobile/http';
|
|
4
|
+
export { preflight as OPTIONS } from '../../../../../lib/mobile/http';
|
|
5
|
+
export const GET = privateRoute(managedIcs);
|
|
6
|
+
export const ALL = () => methodNotAllowed('GET, OPTIONS');
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { getManaged } from '../../../../../lib/mobile/manage';
|
|
2
|
+
import { privateRoute } from '../../../../../lib/mobile/route';
|
|
3
|
+
import { methodNotAllowed } from '../../../../../lib/mobile/http';
|
|
4
|
+
export { preflight as OPTIONS } from '../../../../../lib/mobile/http';
|
|
5
|
+
export const GET = privateRoute(getManaged);
|
|
6
|
+
export const ALL = () => methodNotAllowed('GET, OPTIONS');
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { rescheduleManaged } from '../../../../../lib/mobile/manage';
|
|
2
|
+
import { privateRoute } from '../../../../../lib/mobile/route';
|
|
3
|
+
import { methodNotAllowed } from '../../../../../lib/mobile/http';
|
|
4
|
+
export { preflight as OPTIONS } from '../../../../../lib/mobile/http';
|
|
5
|
+
export const POST = privateRoute(rescheduleManaged, { write: true });
|
|
6
|
+
export const ALL = () => methodNotAllowed('POST, OPTIONS');
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { getForm } from '../../../../../lib/mobile/forms';
|
|
2
|
+
import { readRoute } from '../../../../../lib/mobile/route';
|
|
3
|
+
import { methodNotAllowed } from '../../../../../lib/mobile/http';
|
|
4
|
+
export { preflight as OPTIONS } from '../../../../../lib/mobile/http';
|
|
5
|
+
export const GET = readRoute(getForm, true);
|
|
6
|
+
export const HEAD = GET;
|
|
7
|
+
export const ALL = () => methodNotAllowed();
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { submitFormResponse } from '../../../../../lib/mobile/forms';
|
|
2
|
+
import { privateRoute } from '../../../../../lib/mobile/route';
|
|
3
|
+
import { methodNotAllowed } from '../../../../../lib/mobile/http';
|
|
4
|
+
export { preflight as OPTIONS } from '../../../../../lib/mobile/http';
|
|
5
|
+
export const POST = privateRoute(submitFormResponse, { write: true, status: 201 });
|
|
6
|
+
export const ALL = () => methodNotAllowed('POST, OPTIONS');
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { myBookings } from '../../../../lib/mobile/guest';
|
|
2
|
+
import { privateRoute } from '../../../../lib/mobile/route';
|
|
3
|
+
import { methodNotAllowed } from '../../../../lib/mobile/http';
|
|
4
|
+
export { preflight as OPTIONS } from '../../../../lib/mobile/http';
|
|
5
|
+
export const GET = privateRoute(myBookings);
|
|
6
|
+
export const ALL = () => methodNotAllowed('GET, OPTIONS');
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { requestCode } from '../../../../lib/mobile/guest';
|
|
2
|
+
import { privateRoute } from '../../../../lib/mobile/route';
|
|
3
|
+
import { methodNotAllowed } from '../../../../lib/mobile/http';
|
|
4
|
+
export { preflight as OPTIONS } from '../../../../lib/mobile/http';
|
|
5
|
+
export const POST = privateRoute(requestCode, { write: true, status: 202 });
|
|
6
|
+
export const ALL = () => methodNotAllowed('POST, OPTIONS');
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { revokeGuest } from '../../../../lib/mobile/guest';
|
|
2
|
+
import { privateRoute } from '../../../../lib/mobile/route';
|
|
3
|
+
import { methodNotAllowed } from '../../../../lib/mobile/http';
|
|
4
|
+
export { preflight as OPTIONS } from '../../../../lib/mobile/http';
|
|
5
|
+
export const POST = privateRoute(revokeGuest, { write: true });
|
|
6
|
+
export const ALL = () => methodNotAllowed('POST, OPTIONS');
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { exchangeCode } from '../../../../lib/mobile/guest';
|
|
2
|
+
import { privateRoute } from '../../../../lib/mobile/route';
|
|
3
|
+
import { methodNotAllowed } from '../../../../lib/mobile/http';
|
|
4
|
+
export { preflight as OPTIONS } from '../../../../lib/mobile/http';
|
|
5
|
+
export const POST = privateRoute(exchangeCode, { write: true });
|
|
6
|
+
export const ALL = () => methodNotAllowed('POST, OPTIONS');
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { getLinks } from '../../../lib/mobile/links';
|
|
2
|
+
import { readRoute } from '../../../lib/mobile/route';
|
|
3
|
+
import { methodNotAllowed } from '../../../lib/mobile/http';
|
|
4
|
+
export { preflight as OPTIONS } from '../../../lib/mobile/http';
|
|
5
|
+
export const GET = readRoute(getLinks, true);
|
|
6
|
+
export const HEAD = GET;
|
|
7
|
+
export const ALL = () => methodNotAllowed();
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { search } from '../../../lib/mobile/search';
|
|
2
|
+
import { readRoute } from '../../../lib/mobile/route';
|
|
3
|
+
import { methodNotAllowed } from '../../../lib/mobile/http';
|
|
4
|
+
export { preflight as OPTIONS } from '../../../lib/mobile/http';
|
|
5
|
+
export const GET = readRoute(search, true);
|
|
6
|
+
export const HEAD = GET;
|
|
7
|
+
export const ALL = () => methodNotAllowed();
|
|
@@ -3,7 +3,7 @@ import Site from '../../../layouts/Site.astro';
|
|
|
3
3
|
import BookingFlow from '../../../components/booking/BookingFlow.astro';
|
|
4
4
|
import { getRepo } from '../../../lib/repo';
|
|
5
5
|
import { bookingsEnabled, manageUrl, resourceUrl } from '../../../lib/calendar/config';
|
|
6
|
-
import { availabilityContext, createResourceBooking,
|
|
6
|
+
import { availabilityContext, createResourceBooking, guestMoveLink, guestReschedule, BookingError } from '../../../lib/booking/service';
|
|
7
7
|
import { siteOrigin } from '../../../lib/seo';
|
|
8
8
|
import { waitUntilFrom } from '../../../lib/webhooks';
|
|
9
9
|
|
|
@@ -36,7 +36,7 @@ if (Astro.request.method === 'POST') {
|
|
|
36
36
|
if (token) {
|
|
37
37
|
const b = await repo.getBookingByToken(token);
|
|
38
38
|
if (!b || b.linkId !== link.id) throw new BookingError(404, 'That booking can’t be found.');
|
|
39
|
-
await
|
|
39
|
+
await guestReschedule(repo, b, start, end, flow);
|
|
40
40
|
return Astro.redirect(`${manageUrl(b.manageToken)}?moved=1`);
|
|
41
41
|
}
|
|
42
42
|
const answers: Record<string, string> = {};
|
|
@@ -59,7 +59,7 @@ if (Astro.request.method === 'POST') {
|
|
|
59
59
|
Astro.response.status = err instanceof BookingError ? err.status : 500;
|
|
60
60
|
}
|
|
61
61
|
}
|
|
62
|
-
const ctx = await availabilityContext(repo, resource, link, settings, Date.now(), reschedule?.id);
|
|
62
|
+
const ctx = await availabilityContext(repo, resource, reschedule ? guestMoveLink(link) : link, settings, Date.now(), reschedule?.id);
|
|
63
63
|
---
|
|
64
64
|
|
|
65
65
|
<Site title={`${link.name} · ${resource.name}`} description={link.description || `Book ${resource.name}.`} noindex={link.visibility === 'unlisted' || resource.visibility === 'unlisted'}>
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import Site from '../../../layouts/Site.astro';
|
|
3
3
|
import { getRepo } from '../../../lib/repo';
|
|
4
4
|
import { CALENDAR, bookingLinkUrl, bookingsEnabled, eventUrl, resourceUrl } from '../../../lib/calendar/config';
|
|
5
|
-
import {
|
|
5
|
+
import { guestCancel, guestChanges, BookingError } from '../../../lib/booking/service';
|
|
6
6
|
import { bookingIcs, whenText } from '../../../lib/booking/emails';
|
|
7
7
|
import { resourceZone } from '../../../lib/booking/availability';
|
|
8
8
|
import { eventTimeZone, locationLine } from '../../../lib/calendar/events';
|
|
@@ -38,7 +38,7 @@ if (Astro.request.method === 'POST') {
|
|
|
38
38
|
const form = await Astro.request.formData();
|
|
39
39
|
if (form.get('op') === 'cancel' && (booking.status === 'pending' || booking.status === 'confirmed')) {
|
|
40
40
|
try {
|
|
41
|
-
current = await
|
|
41
|
+
current = await guestCancel(repo, booking, { origin, locals: Astro.locals, waitUntil: waitUntilFrom(Astro.locals) });
|
|
42
42
|
} catch (err) {
|
|
43
43
|
error = err instanceof BookingError ? err.message : 'Couldn’t cancel — please try again.';
|
|
44
44
|
}
|
|
@@ -48,6 +48,7 @@ const tz = event ? eventTimeZone(event, settings) : resource ? resourceZone(reso
|
|
|
48
48
|
const when = whenText(current, tz, settings, link?.mode === 'day' || event?.allDay);
|
|
49
49
|
const active = current.status === 'pending' || current.status === 'confirmed';
|
|
50
50
|
const upcoming = current.end > Date.now();
|
|
51
|
+
const can = guestChanges(current, link);
|
|
51
52
|
const isNew = Astro.url.searchParams.has('new');
|
|
52
53
|
const moved = Astro.url.searchParams.has('moved');
|
|
53
54
|
const heading = current.status === 'cancelled' ? 'Booking cancelled'
|
|
@@ -91,15 +92,18 @@ const googleUrl = `https://calendar.google.com/calendar/render?${new URLSearchPa
|
|
|
91
92
|
</div>
|
|
92
93
|
</details>
|
|
93
94
|
)}
|
|
94
|
-
{resource && link &&
|
|
95
|
+
{resource && link && can.reschedule && (
|
|
95
96
|
<a class="pc-manage-btn" href={`${bookingLinkUrl(resource.slug, link.slug)}?reschedule=${encodeURIComponent(current.manageToken)}`}>Reschedule</a>
|
|
96
97
|
)}
|
|
97
|
-
|
|
98
|
-
<
|
|
99
|
-
|
|
100
|
-
|
|
98
|
+
{can.cancel && (
|
|
99
|
+
<form method="post" onsubmit="return confirm('Cancel this booking?')">
|
|
100
|
+
<input type="hidden" name="op" value="cancel" />
|
|
101
|
+
<button class="pc-manage-btn is-danger" type="submit">Cancel {current.kind === 'rsvp' ? 'RSVP' : 'booking'}</button>
|
|
102
|
+
</form>
|
|
103
|
+
)}
|
|
101
104
|
</div>
|
|
102
105
|
)}
|
|
106
|
+
{can.closed && <p class="pc-manage-note">Changes close {can.cutoffHours} hours before the start. Please contact us to change or cancel.</p>}
|
|
103
107
|
<p class="pc-bk-foot">
|
|
104
108
|
{event ? <a href={eventUrl(event.slug)}>Back to {event.title}</a> : resource ? <a href={resourceUrl(resource.slug)}>Back to {resource.name}</a> : null}
|
|
105
109
|
</p>
|
package/src/styles/admin.css
CHANGED
|
@@ -1590,3 +1590,69 @@ a.bk-row:hover, .bk-row.is-sel { background: var(--hover); }
|
|
|
1590
1590
|
/* Bookings → Day view: a room held by an event (tinted, dashed; links to the event) */
|
|
1591
1591
|
.bk-blk.is-hold { background: color-mix(in srgb, var(--accent) 10%, var(--panel)); border: 1px dashed var(--accent-ring); z-index: 0; }
|
|
1592
1592
|
.bk-blk:not(.is-hold) { z-index: 1; }
|
|
1593
|
+
/* Admin → Mobile app → App access: plain rows, square switches (no pills) */
|
|
1594
|
+
.acc { display: grid; gap: 16px; margin-bottom: 24px; }
|
|
1595
|
+
.acc .card { margin: 0; }
|
|
1596
|
+
.acc-row { display: flex; align-items: center; justify-content: space-between; gap: 16px; padding: 12px 0; border-top: 1px solid var(--line); }
|
|
1597
|
+
.acc .card-head + .acc-row { border-top: 0; }
|
|
1598
|
+
.acc-row > div:first-child { min-width: 0; }
|
|
1599
|
+
.acc-row b { display: block; font-size: 14px; }
|
|
1600
|
+
.acc-row small { display: block; color: var(--muted); font-size: 12.5px; margin-top: 2px; }
|
|
1601
|
+
.acc-row small.acc-warn { color: var(--amber); }
|
|
1602
|
+
.acc-actions { display: flex; align-items: center; gap: 10px; flex: none; }
|
|
1603
|
+
.acc-switch { width: 38px; height: 22px; border-radius: 7px; border: 0; padding: 0; background: var(--line-strong); position: relative; flex: none; cursor: pointer; }
|
|
1604
|
+
.acc-switch i { position: absolute; top: 3px; left: 3px; width: 16px; height: 16px; border-radius: 5px; background: #fff; box-shadow: 0 1px 2px rgb(0 0 0 / 25%); transition: left 0.15s; }
|
|
1605
|
+
.acc-switch.on { background: var(--accent); }
|
|
1606
|
+
.acc-switch.on i { left: 19px; }
|
|
1607
|
+
.acc-switch:focus-visible { outline: 2px solid var(--accent-ring); outline-offset: 2px; }
|
|
1608
|
+
.acc-switch:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
1609
|
+
.acc-row input.acc-num { width: 96px; flex: none; text-align: right; }
|
|
1610
|
+
@media (max-width: 640px) {
|
|
1611
|
+
/* switches and numbers stay beside their label; the role picker takes its own line */
|
|
1612
|
+
.acc-row { flex-wrap: wrap; }
|
|
1613
|
+
.acc-row > div:first-child { flex: 1 1 180px; }
|
|
1614
|
+
.acc-row .seg { width: 100%; }
|
|
1615
|
+
.acc-row .seg > * { flex: 1; }
|
|
1616
|
+
}
|
|
1617
|
+
|
|
1618
|
+
/* Settings → Monitoring (Uptime Kuma) */
|
|
1619
|
+
.mon { display: grid; gap: 16px; }
|
|
1620
|
+
.mon .flash { margin: 0; }
|
|
1621
|
+
.mon-dot { display: inline-block; width: 9px; height: 9px; border-radius: 3px; margin-right: 8px; vertical-align: 1px; background: var(--faint); }
|
|
1622
|
+
.mon-dot.up { background: var(--green); }
|
|
1623
|
+
.mon-dot.down { background: var(--red); }
|
|
1624
|
+
.mon-dot.pending, .mon-dot.maintenance { background: var(--amber); }
|
|
1625
|
+
.mon-self { display: flex; flex-wrap: wrap; align-items: center; gap: 6px 18px; font-size: 13px; color: var(--text-2); padding: 12px 14px; background: var(--panel-2); border-radius: var(--r-md); }
|
|
1626
|
+
.mon-self b { color: var(--text); }
|
|
1627
|
+
.mon-self a { margin-left: auto; color: var(--muted); font-size: 12.5px; }
|
|
1628
|
+
.mon-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 10px; }
|
|
1629
|
+
.mon-tile { background: var(--panel); border: 1px solid var(--line); border-radius: var(--r-md); padding: 12px 14px; min-width: 0; }
|
|
1630
|
+
.mon-tile.is-down { background: var(--red-soft); border-color: rgb(192 59 59 / 25%); }
|
|
1631
|
+
.mon-name { font-weight: 600; font-size: 14px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
1632
|
+
.mon-sub { color: var(--muted); font-size: 12.5px; margin-top: 2px; }
|
|
1633
|
+
.mon-sub .nw { white-space: nowrap; }
|
|
1634
|
+
.mon-msg { color: var(--red); font-size: 12.5px; margin-top: 4px; overflow-wrap: anywhere; }
|
|
1635
|
+
.mon-beats { display: flex; gap: 2px; margin-top: 10px; height: 14px; }
|
|
1636
|
+
.mon-beats i { flex: 1; max-width: 8px; border-radius: 2px; background: var(--green); opacity: 0.75; }
|
|
1637
|
+
.mon-beats i.down { background: var(--red); opacity: 1; }
|
|
1638
|
+
.mon-beats i.pending, .mon-beats i.maintenance { background: var(--amber); }
|
|
1639
|
+
.mon-empty { border: 1px dashed var(--line-strong); border-radius: var(--r-md); padding: 14px 16px; }
|
|
1640
|
+
.mon-empty p { margin: 4px 0 0; color: var(--text-2); font-size: 13px; }
|
|
1641
|
+
.mon-cmd { display: block; margin: 8px 0; padding: 8px 10px; overflow-x: auto; white-space: nowrap; font-size: 12.5px; }
|
|
1642
|
+
.mon-fields { display: grid; gap: 14px; }
|
|
1643
|
+
.mon-field { display: grid; gap: 6px; font-size: 13px; }
|
|
1644
|
+
.mon-field > span { font-weight: 600; color: var(--text); }
|
|
1645
|
+
.mon-field > span small { font-weight: 400; color: var(--muted); margin-left: 6px; }
|
|
1646
|
+
.mon-field > small { color: var(--muted); }
|
|
1647
|
+
.mon-field input { width: 100%; min-width: 0; }
|
|
1648
|
+
.mon-inline { display: flex; gap: 8px; }
|
|
1649
|
+
.mon-inline input { flex: 1; }
|
|
1650
|
+
.mon-inline .btn { flex: none; }
|
|
1651
|
+
.mon-actions { display: flex; justify-content: flex-end; gap: 8px; }
|
|
1652
|
+
@media (max-width: 640px) {
|
|
1653
|
+
.mon-self a { margin-left: 0; }
|
|
1654
|
+
.mon-field > span small { display: block; margin: 2px 0 0; }
|
|
1655
|
+
.mon-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px; }
|
|
1656
|
+
.mon-tile { padding: 10px 12px; }
|
|
1657
|
+
.mon-sub { font-size: 12px; }
|
|
1658
|
+
}
|