Skip to content
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
d694fd1
Add Wishlist to Prisma Schema
renatodellosso Sep 3, 2025
f212a9b
Add db migration
renatodellosso Sep 3, 2025
2d26839
Rename comment field to comments
renatodellosso Sep 3, 2025
f0bd900
Add wishlists to seeding
renatodellosso Sep 3, 2025
afb34ad
Add wishlist types and service
renatodellosso Sep 3, 2025
f00fd6d
Create WishlistStats type
renatodellosso Sep 3, 2025
5e67b53
Add POST api/wishlists
renatodellosso Sep 3, 2025
8198323
Fix return in POST api/wishlists
renatodellosso Sep 3, 2025
2bc32e9
Add GET api/wishlists
renatodellosso Sep 3, 2025
a3900a9
Add PATCH api/wishlists/[wishlistId]. Add authRequirePartner. Add sch…
renatodellosso Sep 3, 2025
6f16318
Add DELETE api/wishlists/[wishlistId[
renatodellosso Sep 3, 2025
c846d8e
Better typing for updateWishlistSchema
renatodellosso Sep 3, 2025
09f4fb7
Make quantity a decimal not an int
renatodellosso Sep 4, 2025
0d9267b
Revert prior change
renatodellosso Sep 4, 2025
cadc4f6
Add note about errors to authRequirePartner
renatodellosso Sep 4, 2025
e0b6ffc
Clean up updateWishlist
renatodellosso Sep 4, 2025
fb48e30
Make UpdateWishlistData build off of CreateWishListData
renatodellosso Sep 4, 2025
38ef77f
Parse PATCH api/wishlists/[id] params with zod
renatodellosso Sep 8, 2025
28a4851
Parse DELETE api/wishlists/[id] params with zod
renatodellosso Sep 8, 2025
aa2f04c
Parse GET api/wishlists params with zod
renatodellosso Sep 8, 2025
f00bbaa
Remove unneeded import
renatodellosso Sep 8, 2025
35be09d
Convert authRequirePartner to isPartner
renatodellosso Sep 8, 2025
188af22
Fix GET /wishlists without ID
renatodellosso Sep 8, 2025
6f5917d
Use isPartner in GET
renatodellosso Sep 8, 2025
0f94927
Remove unneeded import
renatodellosso Sep 8, 2025
5d3f1c7
Misc fixes
renatodellosso Sep 8, 2025
7ce38eb
Fix per wishlist endpoints
renatodellosso Sep 8, 2025
842e3b9
Use queryRaw for counting wishlists
renatodellosso Sep 8, 2025
e93302e
Update src/services/wishlistService.ts
renatodellosso Sep 8, 2025
fc07b3f
Fix typo
renatodellosso Sep 8, 2025
b076184
Update src/app/api/wishlists/[wishlistId]/route.ts
renatodellosso Sep 8, 2025
4a932bc
Update src/app/api/wishlists/route.ts
renatodellosso Sep 8, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions prisma/migrations/20250903023312_create_wishlists/migration.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
-- CreateTable
CREATE TABLE "Wishlist" (
"id" SERIAL NOT NULL,
"name" TEXT NOT NULL,
"unitSize" TEXT NOT NULL,
"quantity" INTEGER NOT NULL,
"priority" "RequestPriority" NOT NULL,
"lastUpdated" TIMESTAMP(3) NOT NULL,
"comment" TEXT NOT NULL,
"partnerId" INTEGER NOT NULL,

CONSTRAINT "Wishlist_pkey" PRIMARY KEY ("id")
);

-- AddForeignKey
ALTER TABLE "Wishlist" ADD CONSTRAINT "Wishlist_partnerId_fkey" FOREIGN KEY ("partnerId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/*
Warnings:

- You are about to drop the column `comment` on the `Wishlist` table. All the data in the column will be lost.
- Added the required column `comments` to the `Wishlist` table without a default value. This is not possible if the table is not empty.

*/
-- AlterTable
ALTER TABLE "Wishlist" DROP COLUMN "comment",
ADD COLUMN "comments" TEXT NOT NULL;
16 changes: 16 additions & 0 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ model User {
distributions Distribution[]
unallocatedItemRequestAllocations UnallocatedItemRequestAllocation[]
signOffs SignOff[]
wishlists Wishlist[]
}

model UserInvite {
Expand Down Expand Up @@ -272,3 +273,18 @@ model SignOff {

distributions Distribution[]
}

model Wishlist {
id Int @id @default(autoincrement())

name String
unitSize String
quantity Int
priority RequestPriority

lastUpdated DateTime @updatedAt

comments String
partnerId Int
partner User @relation(fields: [partnerId], references: [id], onDelete: Cascade)
}
94 changes: 69 additions & 25 deletions scripts/seed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,42 +68,44 @@ const partnerDetailsTemplate = {
orgTitle: "Regional Director",
primaryTelephone: "+509 1234-5678",
secondaryTelephone: "+509 1234-5679",
email: "marie.joseph@hopemedicine.ht"
email: "marie.joseph@hopemedicine.ht",
},
medicalContact: {
firstName: "Jean",
lastName: "Baptiste",
orgTitle: "Chief Medical Officer",
primaryTelephone: "+509 2345-6789",
secondaryTelephone: "+509 2345-6790",
email: "jean.baptiste@hopemedicine.ht"
email: "jean.baptiste@hopemedicine.ht",
},
adminDirectorContact: {
firstName: "Pierre",
lastName: "Morel",
orgTitle: "Administrative Director",
primaryTelephone: "+509 3456-7890",
secondaryTelephone: "+509 3456-7891",
email: "pierre.morel@hopemedicine.ht"
email: "pierre.morel@hopemedicine.ht",
},
pharmacyContact: {
firstName: "Claire",
lastName: "Dumas",
orgTitle: "Chief Pharmacist",
primaryTelephone: "+509 4567-8901",
secondaryTelephone: "+509 4567-8902",
email: "claire.dumas@hopemedicine.ht"
email: "claire.dumas@hopemedicine.ht",
},
contactWhatsAppName: "Dr. Marie Joseph",
contactWhatsAppNumber: "+509 1234-5678",

// Introduction
organizationHistory: "Established to provide quality healthcare services to underserved communities in Haiti. Our mission is to improve health outcomes through accessible medical care and community outreach programs.",
organizationHistory:
"Established to provide quality healthcare services to underserved communities in Haiti. Our mission is to improve health outcomes through accessible medical care and community outreach programs.",
supportRequested: "ongoing_support",
yearOrganizationEstablished: 2015,
registeredWithMssp: true,
proofOfRegistationWithMssp: "mssp_registration_certificate.pdf",
programUpdatesSinceLastReport: "Expanded maternal health services and added mobile clinic outreach program",
programUpdatesSinceLastReport:
"Expanded maternal health services and added mobile clinic outreach program",

// Facility
facilityType: ["clinic", "primary_care", "health_center"],
Expand All @@ -114,10 +116,12 @@ const partnerDetailsTemplate = {
numberOfInpatientBeds: 25,
numberOfPatientsServedAnnually: 12000,
communityMobileOutreachOffered: true,
communityMobileOutreachDescription: "Weekly mobile clinics to remote communities, providing basic healthcare and health education",
communityMobileOutreachDescription:
"Weekly mobile clinics to remote communities, providing basic healthcare and health education",

// Infrastructure and Services
facilityDescription: "Modern healthcare facility with emergency care, maternal health, pediatrics, and pharmacy services",
facilityDescription:
"Modern healthcare facility with emergency care, maternal health, pediatrics, and pharmacy services",
cleanWaterAccessible: true,
cleanWaterDescription: "Municipal water supply with backup well water system",
closestSourceOfCleanWater: "",
Expand All @@ -126,17 +130,26 @@ const partnerDetailsTemplate = {
electricityAvailable: true,
accessibleByDisablePatients: true,
medicationDisposalProcessDefined: true,
medicationDisposalProcessDescription: "Proper pharmaceutical waste disposal following WHO guidelines with licensed waste management company",
medicationDisposalProcessDescription:
"Proper pharmaceutical waste disposal following WHO guidelines with licensed waste management company",
pickupVehiclePresent: true,
pickupVehicleType: "Ambulance and supply truck",
pickupLocations: ["port_au_prince"],

// Programs and Services Provided
medicalServicesProvided: ["pediatrics", "maternal_care", "lab_tests", "immunizations", "chronic_diseases"],
otherMedicalServicesProvided: "Health education, Community outreach, Vaccination programs",
medicalServicesProvided: [
"pediatrics",
"maternal_care",
"lab_tests",
"immunizations",
"chronic_diseases",
],
otherMedicalServicesProvided:
"Health education, Community outreach, Vaccination programs",

// Finances
patientsWhoCannotPay: "Sliding scale fees and charity care program available for patients who cannot afford treatment",
patientsWhoCannotPay:
"Sliding scale fees and charity care program available for patients who cannot afford treatment",
percentageOfPatientsNeedingFinancialAid: 65,
percentageOfPatientsReceivingFreeTreatment: 30,
annualSpendingOnMedicationsAndMedicalSupplies: "50001_to_100000",
Expand Down Expand Up @@ -170,8 +183,15 @@ const partnerDetailsTemplate = {
other: "2 lab technicians, 1 social worker",

// Medical Supplies
mostNeededMedicalSupplies: ["anti_infectives", "cardiovascular", "bandages", "syringes_needles", "vitamins_minerals"],
otherSpecialityItemsNeeded: "Blood pressure monitors, glucometers, wound care supplies"
mostNeededMedicalSupplies: [
"anti_infectives",
"cardiovascular",
"bandages",
"syringes_needles",
"vitamins_minerals",
],
otherSpecialityItemsNeeded:
"Blood pressure monitors, glucometers, wound care supplies",
};

const generalItems: Array<GeneralItem> = [
Expand Down Expand Up @@ -275,7 +295,9 @@ function genItem(props: Partial<Item> = {}): Omit<Item, "id"> {
const template = pick(itemTemplates);
return {
...template,
expirationDate: template.expirationDate ? new Date(template.expirationDate) : null,
expirationDate: template.expirationDate
? new Date(template.expirationDate)
: null,
donorName: pick(donorNames),
quantity: randInt(1, 10) * 10,
lotNumber: pick(lots),
Expand Down Expand Up @@ -365,17 +387,17 @@ async function run() {
// Vary some details to make each partner unique
yearOrganizationEstablished: 2010 + (i % 10),
numberOfInpatientBeds: 15 + (i % 20),
numberOfPatientsServedAnnually: 8000 + (i * 500),
numberOfPatientsServedAnnually: 8000 + i * 500,
numberOfDoctors: 2 + (i % 5),
numberOfNurses: 8 + (i % 10),
numberOfMidwives: 1 + (i % 4),
numberOfPharmacists: 1 + (i % 3),
totalNumberOfStaff: 30 + (i % 25),
totalPatientsServedLastYear: 7500 + (i * 400),
menServedLastYear: 2000 + (i * 100),
womenServedLastYear: 2500 + (i * 120),
boysServedLastYear: 1500 + (i * 80),
girlsServedLastYear: 1500 + (i * 80),
totalPatientsServedLastYear: 7500 + i * 400,
menServedLastYear: 2000 + i * 100,
womenServedLastYear: 2500 + i * 120,
boysServedLastYear: 1500 + i * 80,
girlsServedLastYear: 1500 + i * 80,
},
})),
});
Expand Down Expand Up @@ -411,6 +433,20 @@ async function run() {
}),
});

await tx.wishlist.createManyAndReturn({
data: Array.from({ length: 20 }, () => {
const genItem = pick(generalItems);
return {
partnerId: pick(partners).id,
name: genItem.title,
unitSize: genItem.unitType,
priority: pick(Object.values(RequestPriority)) as RequestPriority,
quantity: randInt(1, 4) * 5,
comments: "pls",
};
}),
});

await Promise.all(
(
[
Expand All @@ -429,7 +465,9 @@ async function run() {
partnerId: partner.id,
quantity: randInt(1, 8),
comments: "pls give me this",
priority: pick(Object.values(RequestPriority)) as RequestPriority,
priority: pick(
Object.values(RequestPriority)
) as RequestPriority,
})),
},
})),
Expand All @@ -455,7 +493,9 @@ async function run() {
partnerId: partner.id,
quantity: randInt(1, 8),
comments: "pls give me this",
priority: pick(Object.values(RequestPriority)) as RequestPriority,
priority: pick(
Object.values(RequestPriority)
) as RequestPriority,
})),
},
})),
Expand All @@ -481,7 +521,9 @@ async function run() {
partnerId: partner.id,
quantity: randInt(1, 8),
comments: "pls give me this",
priority: pick(Object.values(RequestPriority)) as RequestPriority,
priority: pick(
Object.values(RequestPriority)
) as RequestPriority,
})),
},
})),
Expand All @@ -507,7 +549,9 @@ async function run() {
partnerId: partner.id,
quantity: randInt(1, 8),
comments: "pls give me this",
priority: pick(Object.values(RequestPriority)) as RequestPriority,
priority: pick(
Object.values(RequestPriority)
) as RequestPriority,
})),
},
})),
Expand Down
79 changes: 79 additions & 0 deletions src/app/api/wishlists/[wishlistId]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { updateWishlistSchema } from "@/schema/wishlist";
import UserService from "@/services/userService";
import { WishlistService } from "@/services/wishlistService";
import { ArgumentError, errorResponse, ok } from "@/util/errors";
import { NextRequest } from "next/server";

/**
* Updates a wishlist item. Allows changes to name, unit size, quantity, priority, and comments.
* @param req
* @returns
*/
export async function PATCH(
req: NextRequest,
{ params }: { params: Promise<{ wishlistId: string }> }
) {
try {
const session = await UserService.authRequirePartner();

const { wishlistId } = await params;
Comment thread
renatodellosso marked this conversation as resolved.
Outdated
const body = await req.json();

const parsed = updateWishlistSchema.safeParse(body);
if (!parsed.success) {
throw new ArgumentError(parsed.error.message);
}

const wishlistItem = await WishlistService.getWishlistItem(
Number(wishlistId)
);

// Check ID first to avoid revealing the existence/non-existence of the wishlist item to an unauthorized user
if (wishlistItem?.partnerId !== Number(session.user.id)) {
throw new ArgumentError("Cannot update another partner's wishlist item");
}

if (!wishlistItem) {
throw new ArgumentError("Wishlist item not found");
}
Comment thread
renatodellosso marked this conversation as resolved.

await WishlistService.updateWishlist({
id: Number(wishlistId),
...parsed.data,
});

return ok();
} catch (error) {
return errorResponse(error);
}
}

export async function DELETE(
_: NextRequest,
{ params }: { params: Promise<{ wishlistId: string }> }
) {
try {
const session = await UserService.authRequirePartner();

const { wishlistId } = await params;
Comment thread
renatodellosso marked this conversation as resolved.
Outdated

const wishlistItem = await WishlistService.getWishlistItem(
Number(wishlistId)
);

// Check ID first to avoid revealing the existence/non-existence of the wishlist item to an unauthorized user
if (wishlistItem?.partnerId !== Number(session.user.id)) {
throw new ArgumentError("Cannot delete another partner's wishlist item");
}

if (!wishlistItem) {
throw new ArgumentError("Wishlist item not found");
}
Comment thread
renatodellosso marked this conversation as resolved.

await WishlistService.deleteWishlist(Number(wishlistId));

return ok();
} catch (error) {
return errorResponse(error);
}
}
Loading
Loading