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;
+}