382 lines
13 KiB
TypeScript
382 lines
13 KiB
TypeScript
"use server";
|
|
|
|
import { and, eq } from "drizzle-orm";
|
|
import { revalidatePath } from "next/cache";
|
|
import { z } from "zod";
|
|
import type { ApiAuthContext } from "@/lib/api-auth";
|
|
import { db } from "@/lib/db";
|
|
import { getCurrentSession } from "@/lib/session";
|
|
import { logActivityForScope } from "@/modules/_core/activity";
|
|
import { cancelReminder, listReminderOffsets } from "@/modules/_core/reminders";
|
|
import { petAppointments, petPrescriptions, pets, petVaccinations } from "../schema";
|
|
import { syncPetAppointmentReminders, syncPetDueDateReminder } from "./reminders";
|
|
import {
|
|
appointmentInput,
|
|
appointmentUpdateInput,
|
|
petInput,
|
|
petUpdateInput,
|
|
prescriptionInput,
|
|
prescriptionUpdateInput,
|
|
vaccinationInput,
|
|
vaccinationUpdateInput,
|
|
} from "./schemas";
|
|
|
|
type Scope = Pick<ApiAuthContext, "householdId" | "userId">;
|
|
const idInput = z.object({ id: z.string().uuid() });
|
|
|
|
async function requirePet(scope: Scope, id: string) {
|
|
const [pet] = await db
|
|
.select()
|
|
.from(pets)
|
|
.where(and(eq(pets.id, id), eq(pets.householdId, scope.householdId)))
|
|
.limit(1);
|
|
if (!pet) throw new Error("Pet not found");
|
|
return pet;
|
|
}
|
|
|
|
async function requireAppointment(scope: Scope, id: string) {
|
|
const [row] = await db
|
|
.select()
|
|
.from(petAppointments)
|
|
.where(and(eq(petAppointments.id, id), eq(petAppointments.householdId, scope.householdId)))
|
|
.limit(1);
|
|
if (!row) throw new Error("Record not found");
|
|
return row;
|
|
}
|
|
async function requireVaccination(scope: Scope, id: string) {
|
|
const [row] = await db
|
|
.select()
|
|
.from(petVaccinations)
|
|
.where(and(eq(petVaccinations.id, id), eq(petVaccinations.householdId, scope.householdId)))
|
|
.limit(1);
|
|
if (!row) throw new Error("Record not found");
|
|
return row;
|
|
}
|
|
async function requirePrescription(scope: Scope, id: string) {
|
|
const [row] = await db
|
|
.select()
|
|
.from(petPrescriptions)
|
|
.where(and(eq(petPrescriptions.id, id), eq(petPrescriptions.householdId, scope.householdId)))
|
|
.limit(1);
|
|
if (!row) throw new Error("Record not found");
|
|
return row;
|
|
}
|
|
|
|
function revalidatePet(id: string) {
|
|
revalidatePath("/pets");
|
|
revalidatePath(`/pets/${id}`);
|
|
}
|
|
|
|
export async function createPetForScope(scope: Scope, input: z.input<typeof petInput>) {
|
|
const parsed = petInput.parse(input);
|
|
const [pet] = await db
|
|
.insert(pets)
|
|
.values({
|
|
householdId: scope.householdId,
|
|
...parsed,
|
|
breed: parsed.breed ?? null,
|
|
birthDate: parsed.birthDate ?? null,
|
|
notes: parsed.notes ?? null,
|
|
primaryImageUrl: parsed.primaryImageUrl ?? parsed.images[0] ?? null,
|
|
})
|
|
.returning();
|
|
if (!pet) throw new Error("Pet was not created");
|
|
await logActivityForScope(scope, {
|
|
entityType: "pets.pet",
|
|
entityId: pet.id,
|
|
action: "create",
|
|
payload: { name: pet.name },
|
|
});
|
|
return pet;
|
|
}
|
|
export async function createPet(input: z.input<typeof petInput>) {
|
|
const { household, user } = await getCurrentSession();
|
|
const pet = await createPetForScope({ householdId: household.id, userId: user.id }, input);
|
|
revalidatePet(pet.id);
|
|
return pet;
|
|
}
|
|
|
|
export async function updatePetForScope(
|
|
scope: Scope,
|
|
input: { id: string } & z.input<typeof petUpdateInput>,
|
|
) {
|
|
const parsed = idInput.and(petUpdateInput).parse(input);
|
|
await requirePet(scope, parsed.id);
|
|
const { id, ...changes } = parsed;
|
|
await db
|
|
.update(pets)
|
|
.set({ ...changes, updatedAt: new Date() })
|
|
.where(eq(pets.id, id));
|
|
await logActivityForScope(scope, { entityType: "pets.pet", entityId: id, action: "update" });
|
|
}
|
|
export async function updatePet(input: { id: string } & z.input<typeof petUpdateInput>) {
|
|
const { household, user } = await getCurrentSession();
|
|
await updatePetForScope({ householdId: household.id, userId: user.id }, input);
|
|
revalidatePet(input.id);
|
|
}
|
|
|
|
export async function deletePetForScope(scope: Scope, input: { id: string }) {
|
|
const { id } = idInput.parse(input);
|
|
const pet = await requirePet(scope, id);
|
|
await logActivityForScope(scope, {
|
|
entityType: "pets.pet",
|
|
entityId: id,
|
|
action: "delete",
|
|
payload: { name: pet.name },
|
|
});
|
|
await db.delete(pets).where(eq(pets.id, id));
|
|
}
|
|
export async function deletePet(input: { id: string }) {
|
|
const { household, user } = await getCurrentSession();
|
|
await deletePetForScope({ householdId: household.id, userId: user.id }, input);
|
|
revalidatePath("/pets");
|
|
}
|
|
|
|
export async function createAppointmentForScope(
|
|
scope: Scope,
|
|
input: z.input<typeof appointmentInput>,
|
|
) {
|
|
const parsed = appointmentInput.parse(input);
|
|
const pet = await requirePet(scope, parsed.petId);
|
|
const [row] = await db
|
|
.insert(petAppointments)
|
|
.values({
|
|
householdId: scope.householdId,
|
|
petId: parsed.petId,
|
|
title: parsed.title,
|
|
appointmentAt: parsed.appointmentAt,
|
|
clinic: parsed.clinic ?? null,
|
|
notes: parsed.notes ?? null,
|
|
})
|
|
.returning();
|
|
if (!row) throw new Error("Appointment was not created");
|
|
if (scope.userId)
|
|
await syncPetAppointmentReminders({
|
|
householdId: scope.householdId,
|
|
appointmentId: row.id,
|
|
appointmentAt: row.appointmentAt,
|
|
createdBy: scope.userId,
|
|
petName: pet.name,
|
|
title: row.title,
|
|
offsets: parsed.reminderOffsets,
|
|
});
|
|
return row;
|
|
}
|
|
export async function updateAppointmentForScope(
|
|
scope: Scope,
|
|
input: { id: string } & z.input<typeof appointmentUpdateInput>,
|
|
) {
|
|
const parsed = idInput.and(appointmentUpdateInput).parse(input);
|
|
const row = await requireAppointment(scope, parsed.id);
|
|
const pet = await requirePet(scope, row.petId);
|
|
const offsets = parsed.reminderOffsets ?? (await listReminderOffsets("pets.appointment", row.id));
|
|
const { id, reminderOffsets: _, ...changes } = parsed;
|
|
const [updated] = await db
|
|
.update(petAppointments)
|
|
.set({ ...changes, updatedAt: new Date() })
|
|
.where(eq(petAppointments.id, id))
|
|
.returning();
|
|
if (!updated) throw new Error("Appointment not found");
|
|
if (scope.userId)
|
|
await syncPetAppointmentReminders({
|
|
householdId: scope.householdId,
|
|
appointmentId: id,
|
|
appointmentAt: updated.appointmentAt,
|
|
createdBy: scope.userId,
|
|
petName: pet.name,
|
|
title: updated.title,
|
|
offsets,
|
|
});
|
|
return updated;
|
|
}
|
|
export async function deleteAppointmentForScope(scope: Scope, input: { id: string }) {
|
|
const { id } = idInput.parse(input);
|
|
await requireAppointment(scope, id);
|
|
await cancelReminder("pets.appointment", id);
|
|
await db.delete(petAppointments).where(eq(petAppointments.id, id));
|
|
}
|
|
|
|
export async function createAppointment(input: z.input<typeof appointmentInput>) {
|
|
const { household, user } = await getCurrentSession();
|
|
const row = await createAppointmentForScope(
|
|
{ householdId: household.id, userId: user.id },
|
|
input,
|
|
);
|
|
revalidatePet(row.petId);
|
|
return row;
|
|
}
|
|
export async function updateAppointment(
|
|
input: { id: string } & z.input<typeof appointmentUpdateInput>,
|
|
) {
|
|
const { household, user } = await getCurrentSession();
|
|
const row = await updateAppointmentForScope(
|
|
{ householdId: household.id, userId: user.id },
|
|
input,
|
|
);
|
|
revalidatePet(row.petId);
|
|
return row;
|
|
}
|
|
export async function deleteAppointment(input: { id: string }) {
|
|
const { household, user } = await getCurrentSession();
|
|
const row = await requireAppointment({ householdId: household.id, userId: user.id }, input.id);
|
|
await deleteAppointmentForScope({ householdId: household.id, userId: user.id }, input);
|
|
revalidatePet(row.petId);
|
|
}
|
|
|
|
export async function createVaccinationForScope(
|
|
scope: Scope,
|
|
input: z.input<typeof vaccinationInput>,
|
|
) {
|
|
const parsed = vaccinationInput.parse(input);
|
|
const pet = await requirePet(scope, parsed.petId);
|
|
const [row] = await db
|
|
.insert(petVaccinations)
|
|
.values({
|
|
householdId: scope.householdId,
|
|
...parsed,
|
|
dueOn: parsed.dueOn ?? null,
|
|
provider: parsed.provider ?? null,
|
|
notes: parsed.notes ?? null,
|
|
})
|
|
.returning();
|
|
if (!row) throw new Error("Vaccination was not created");
|
|
if (scope.userId)
|
|
await syncPetDueDateReminder({
|
|
householdId: scope.householdId,
|
|
entityType: "pets.vaccination",
|
|
entityId: row.id,
|
|
dueOn: row.dueOn,
|
|
createdBy: scope.userId,
|
|
title: `Vaccination due: ${pet.name}`,
|
|
body: row.name,
|
|
});
|
|
return row;
|
|
}
|
|
export async function updateVaccinationForScope(
|
|
scope: Scope,
|
|
input: { id: string } & z.input<typeof vaccinationUpdateInput>,
|
|
) {
|
|
const parsed = idInput.and(vaccinationUpdateInput).parse(input);
|
|
const row = await requireVaccination(scope, parsed.id);
|
|
const pet = await requirePet(scope, row.petId);
|
|
const { id, ...changes } = parsed;
|
|
const [updated] = await db
|
|
.update(petVaccinations)
|
|
.set({ ...changes, updatedAt: new Date() })
|
|
.where(eq(petVaccinations.id, id))
|
|
.returning();
|
|
if (!updated) throw new Error("Vaccination not found");
|
|
if (scope.userId)
|
|
await syncPetDueDateReminder({
|
|
householdId: scope.householdId,
|
|
entityType: "pets.vaccination",
|
|
entityId: id,
|
|
dueOn: updated.dueOn,
|
|
createdBy: scope.userId,
|
|
title: `Vaccination due: ${pet.name}`,
|
|
body: updated.name,
|
|
});
|
|
return updated;
|
|
}
|
|
export async function deleteVaccinationForScope(scope: Scope, input: { id: string }) {
|
|
const { id } = idInput.parse(input);
|
|
await requireVaccination(scope, id);
|
|
await cancelReminder("pets.vaccination", id);
|
|
await db.delete(petVaccinations).where(eq(petVaccinations.id, id));
|
|
}
|
|
|
|
export async function createVaccination(input: z.input<typeof vaccinationInput>) {
|
|
const { household, user } = await getCurrentSession();
|
|
const row = await createVaccinationForScope(
|
|
{ householdId: household.id, userId: user.id },
|
|
input,
|
|
);
|
|
revalidatePet(row.petId);
|
|
return row;
|
|
}
|
|
export async function deleteVaccination(input: { id: string }) {
|
|
const { household, user } = await getCurrentSession();
|
|
const row = await requireVaccination({ householdId: household.id, userId: user.id }, input.id);
|
|
await deleteVaccinationForScope({ householdId: household.id, userId: user.id }, input);
|
|
revalidatePet(row.petId);
|
|
}
|
|
|
|
export async function createPrescriptionForScope(
|
|
scope: Scope,
|
|
input: z.input<typeof prescriptionInput>,
|
|
) {
|
|
const parsed = prescriptionInput.parse(input);
|
|
const pet = await requirePet(scope, parsed.petId);
|
|
const [row] = await db
|
|
.insert(petPrescriptions)
|
|
.values({
|
|
householdId: scope.householdId,
|
|
...parsed,
|
|
instructions: parsed.instructions ?? null,
|
|
prescribedOn: parsed.prescribedOn ?? null,
|
|
expiresOn: parsed.expiresOn ?? null,
|
|
refillsRemaining: parsed.refillsRemaining ?? null,
|
|
})
|
|
.returning();
|
|
if (!row) throw new Error("Prescription was not created");
|
|
if (scope.userId)
|
|
await syncPetDueDateReminder({
|
|
householdId: scope.householdId,
|
|
entityType: "pets.prescription",
|
|
entityId: row.id,
|
|
dueOn: row.active ? row.expiresOn : null,
|
|
createdBy: scope.userId,
|
|
title: `Prescription due: ${pet.name}`,
|
|
body: row.medication,
|
|
});
|
|
return row;
|
|
}
|
|
export async function updatePrescriptionForScope(
|
|
scope: Scope,
|
|
input: { id: string } & z.input<typeof prescriptionUpdateInput>,
|
|
) {
|
|
const parsed = idInput.and(prescriptionUpdateInput).parse(input);
|
|
const row = await requirePrescription(scope, parsed.id);
|
|
const pet = await requirePet(scope, row.petId);
|
|
const { id, ...changes } = parsed;
|
|
const [updated] = await db
|
|
.update(petPrescriptions)
|
|
.set({ ...changes, updatedAt: new Date() })
|
|
.where(eq(petPrescriptions.id, id))
|
|
.returning();
|
|
if (!updated) throw new Error("Prescription not found");
|
|
if (scope.userId)
|
|
await syncPetDueDateReminder({
|
|
householdId: scope.householdId,
|
|
entityType: "pets.prescription",
|
|
entityId: id,
|
|
dueOn: updated.active ? updated.expiresOn : null,
|
|
createdBy: scope.userId,
|
|
title: `Prescription due: ${pet.name}`,
|
|
body: updated.medication,
|
|
});
|
|
return updated;
|
|
}
|
|
export async function deletePrescriptionForScope(scope: Scope, input: { id: string }) {
|
|
const { id } = idInput.parse(input);
|
|
await requirePrescription(scope, id);
|
|
await cancelReminder("pets.prescription", id);
|
|
await db.delete(petPrescriptions).where(eq(petPrescriptions.id, id));
|
|
}
|
|
|
|
export async function createPrescription(input: z.input<typeof prescriptionInput>) {
|
|
const { household, user } = await getCurrentSession();
|
|
const row = await createPrescriptionForScope(
|
|
{ householdId: household.id, userId: user.id },
|
|
input,
|
|
);
|
|
revalidatePet(row.petId);
|
|
return row;
|
|
}
|
|
export async function deletePrescription(input: { id: string }) {
|
|
const { household, user } = await getCurrentSession();
|
|
const row = await requirePrescription({ householdId: household.id, userId: user.id }, input.id);
|
|
await deletePrescriptionForScope({ householdId: household.id, userId: user.id }, input);
|
|
revalidatePet(row.petId);
|
|
}
|