diff --git a/.env b/.env index 75a7eacd..649bfc58 100644 --- a/.env +++ b/.env @@ -7,4 +7,7 @@ ADAMRMS_EMAIL= ADAMRMS_PASSWORD= ADAMRMS_BASE=https://dash.adam-rms.com ADAMRMS_PROJECT_TYPE_ID=18 +ADAMRMS_TENTATIVE_STATUS_ID=1051 +ADAMRMS_CONFIRMED_STATUS_ID=1053 +ADAMRMS_CANCELLED_STATUS_ID=1058 SESSION_SECRET=change me diff --git a/app/(authenticated)/calendar/[eventID]/EventActionsUI.tsx b/app/(authenticated)/calendar/[eventID]/EventActionsUI.tsx index de1c67c5..205ee790 100644 --- a/app/(authenticated)/calendar/[eventID]/EventActionsUI.tsx +++ b/app/(authenticated)/calendar/[eventID]/EventActionsUI.tsx @@ -95,7 +95,23 @@ export function EventActionsUI(props: { event: EventObjectType }) { }, onConfirm() { startTransition(async () => { - await reinstateEvent(props.event.event_id); + const result = await reinstateEvent(props.event.event_id); + if (!result.ok) { + modals.openModal({ + id: "reinstate-error", + title: "Error", + children: ( + <> + {result.errors?.root ?? `Unknown error (${JSON.stringify(result)})`} + + + ), + }); + } }); }, }); diff --git a/app/(authenticated)/calendar/[eventID]/actions.ts b/app/(authenticated)/calendar/[eventID]/actions.ts index 2da706b1..3ae39a61 100644 --- a/app/(authenticated)/calendar/[eventID]/actions.ts +++ b/app/(authenticated)/calendar/[eventID]/actions.ts @@ -220,7 +220,25 @@ export async function reinstateEvent(eventID: number) { }; } - await Calendar.reinstateEvent(eventID); + const result = await Calendar.reinstateEvent(eventID); + if (!result.ok) { + switch (result.error) { + case "kit_clash": + return { + ok: false, + errors: { + root: "Reinstating this production would result in a kit clash. Please contact the Tech Team.", + }, + }; + default: + return { + ok: false, + errors: { + root: "An unknown error occurred (" + result.error + ")", + }, + }; + }; + } revalidatePath(`/calendar/${event.event_id}`); revalidatePath("/calendar"); diff --git a/features/calendar/adamRMS.ts b/features/calendar/adamRMS.ts index e1e10d53..76f37e94 100644 --- a/features/calendar/adamRMS.ts +++ b/features/calendar/adamRMS.ts @@ -31,6 +31,11 @@ export async function addProjectToAdamRMS( event.end_date, "deliver_dates", ); + await AdamRMS.setProjectStatus( + projectId, + // No idea why you'd link a cancelled project to the RMS, but you do you... + event.is_cancelled ? "cancelled" : event.is_tentative ? "tentative" : "confirmed", + ); await prisma.event.update({ where: { event_id: eventID, @@ -85,6 +90,10 @@ export async function linkAdamRMS(eventID: number, projectID: number) { event.end_date, "dates", ); + await AdamRMS.setProjectStatus( + projectID, + event.is_cancelled ? "cancelled" : event.is_tentative ? "tentative" : "confirmed", + ); await prisma.event.update({ where: { event_id: eventID, diff --git a/features/calendar/events.ts b/features/calendar/events.ts index 0e1dba02..292ed14f 100644 --- a/features/calendar/events.ts +++ b/features/calendar/events.ts @@ -195,9 +195,9 @@ export async function updateEvent( // We use a raw query to get the event info so that we can SELECT FOR UPDATE, // otherwise this risks a race condition. const events = await $db.$queryRaw< - { adam_rms_project_id: number | null; start_date: Date; end_date: Date }[] + { adam_rms_project_id: number | null; start_date: Date; end_date: Date; is_tentative: boolean; is_cancelled: boolean; }[] >` - SELECT adam_rms_project_id, start_date, end_date FROM events WHERE event_id = ${eventID} FOR UPDATE`; + SELECT adam_rms_project_id, start_date, end_date, is_tentative, is_cancelled FROM events WHERE event_id = ${eventID} FOR UPDATE`; if (events.length === 0) { throw new Error("Event not found"); } @@ -207,6 +207,8 @@ export async function updateEvent( // This is because AdamRMS will reject the request if the dates would cause a kit clash. // We change the "deliver dates" first, because this actually triggers the kit clash check. // Then if it succeeds we update it locally and update the event dates to match. + // Similarly, if the status has changed, we need to update that, as un-cancelling an event + // can have the same effect. if ( event.adam_rms_project_id && (event.start_date.getTime() !== data.start_date.getTime() || @@ -222,6 +224,13 @@ export async function updateEvent( return { ok: false, error: "kit_clash" }; } } + + // It's not possible to reinstate an even through this function, so we don't need to worry about + // the status changing from cancelled to uncancelled. In other words, cancelled trumps tentative. + if (!event.is_cancelled && event.adam_rms_project_id && event.is_tentative !== data.is_tentative) { + await AdamRMS.setProjectStatus(event.adam_rms_project_id, data.is_tentative ? "tentative" : "confirmed"); + } + const result = await $db.event.update({ where: { event_id: eventID, @@ -288,25 +297,51 @@ export async function updateEventAttendeeStatus( } export async function cancelEvent(eventID: number) { - await prisma.event.update({ + const res = await prisma.event.update({ where: { event_id: eventID, }, data: { is_cancelled: true, }, + select: { + adam_rms_project_id: true, + } }); + if (res.adam_rms_project_id) { + await AdamRMS.setProjectStatus(res.adam_rms_project_id, "cancelled"); + } } export async function reinstateEvent(eventID: number) { - await prisma.event.update({ - where: { - event_id: eventID, - }, - data: { - is_cancelled: false, - }, - }); + // Watch out! Cancelling an event in AdamRMS releases its assets, so un-cancelling + // an event can cause a kit clash. We need to check this first. + // Do it inside a transaction with a SELECT FOR UPDATE. + return await prisma.$transaction(async $db => { + const events = await $db.$queryRaw<{ adam_rms_project_id: number | null; }[]>` + SELECT adam_rms_project_id FROM events WHERE event_id = ${eventID} FOR UPDATE`; + if (events.length === 0) { + throw new Error("Event not found"); + } + const event = events[0]; + + if (event.adam_rms_project_id) { + const changed = await AdamRMS.setProjectStatus(event.adam_rms_project_id, "confirmed"); + if (!changed) { + return { ok: false, error: "kit_clash" }; + } + } + + await $db.event.update({ + where: { + event_id: eventID, + }, + data: { + is_cancelled: false, + }, + }); + return { ok: true }; + }) } export async function deleteEvent(eventID: number, userID: number) { diff --git a/lib/adamrms/index.ts b/lib/adamrms/index.ts index ec021f71..ab0f0ed1 100644 --- a/lib/adamrms/index.ts +++ b/lib/adamrms/index.ts @@ -103,3 +103,30 @@ export async function newQuickProjectComment( text: comment, })) as {}; } + +export async function setProjectStatus(projectID: number, status: "tentative" | "confirmed" | "cancelled") { + let statusIDStr; + switch (status) { + case "tentative": + statusIDStr = process.env.ADAMRMS_TENTATIVE_STATUS_ID; + break; + case "confirmed": + statusIDStr = process.env.ADAMRMS_CONFIRMED_STATUS_ID; + break; + case "cancelled": + statusIDStr = process.env.ADAMRMS_CANCELLED_STATUS_ID; + break; + default: + invariant(false, `Invalid project status ${status}`);; + } + invariant(statusIDStr, "Missing status ID for status " + status); + const statusID = parseInt(statusIDStr, 10); + + // We still need to check for kit clashes - it's possible that changing a cancelled + // project to un-cancelled will cause a clash. + const res = await makeRequest("/projects/changeStatus.php", "POST", { + projects_id: projectID.toString(10), + projects_status: statusID.toString(10), + }) as { changed: boolean }; + return res.changed; +}