Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
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
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
"email:dev": "email dev --dir ./src/email --port 4000"
},
"dependencies": {
"openai": "^4.63.0",
"@azure/storage-blob": "^12.27.0",
"@headlessui/react": "^2.2.1",
"@phosphor-icons/react": "^2.1.7",
Expand All @@ -31,6 +30,7 @@
"highs": "^1.8.0",
"next": "15.0.4",
"next-auth": "^5.0.0-beta.25",
"openai": "^4.104.0",
"papaparse": "^5.5.2",
"promisify": "^0.0.3",
"react-hot-toast": "^2.4.1",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "GeneralItem" ADD COLUMN "description" TEXT;
2 changes: 2 additions & 0 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,8 @@ model GeneralItem {

initialQuantity Int
requestQuantity Int?

description String?

requests GeneralItemRequest[]
items LineItem[]
Expand Down
18 changes: 16 additions & 2 deletions src/app/api/donorOffers/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
} from "@/util/errors";
import FileService from "@/services/fileService";
import { tableParamsSchema } from "@/schema/tableParams";
import { DescriptionService } from "@/services/descriptionService";

export async function POST(req: NextRequest) {
try {
Expand All @@ -24,16 +25,29 @@ export async function POST(req: NextRequest) {
"You are not allowed to create donor offers"
);
}

const params = req.nextUrl.searchParams;
const formData = await req.formData();
const file = formData.get("file") as File | null;
const preview = formData.get("preview") === "true";
const preview = params.get("preview") === "true";

if (!file) {
throw new ArgumentError("File is required");
}

const parsedFileData = await FileService.parseDonorOfferFile(file);
const itemDescriptionInput = parsedFileData.data.map((item) => ({
title: item["title"] as string,
type: item["type"] as string,
unitType: item["unitType"] as string,
}));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logic: Missing validation for required fields. If title, type, or unitType are undefined/null in parsedFileData.data, this will pass invalid data to DescriptionService


const descriptions =
await DescriptionService.getOrGenerateDescriptions(itemDescriptionInput);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logic: No error handling if getOrGenerateDescriptions fails or returns fewer descriptions than expected. This could lead to undefined values being assigned on line 48


parsedFileData.data.forEach((item, index) => {
item.description = descriptions[index];
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logic: If descriptions.length !== parsedFileData.data.length, array index mismatch will assign wrong descriptions or undefined to items


const result = await DonorOfferService.createDonorOffer(
formData,
parsedFileData,
Expand Down
11 changes: 7 additions & 4 deletions src/app/donorOffers/create/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,9 @@ export default function CreateDonorOfferPage() {
handleFileChange,
resetUpload: resetFileUpload,
} = useFileUpload<{ donorOfferItems: DonorOfferItem[] }>({
previewEndpoint: "/api/donorOffers/create?preview=true",
previewEndpoint: "/api/donorOffers?preview=true",
onSuccess: (result) => {
console.log(result);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style: Remove debug console.log before merging

setData(result.donorOfferItems);
},
validateBeforeUpload: () => {
Expand Down Expand Up @@ -93,7 +94,7 @@ export default function CreateDonorOfferPage() {

const resetUpload = () => {
resetFileUpload();

setPreview(false);
setIsSuccess(false);
setIsError(false);
Expand All @@ -119,7 +120,7 @@ export default function CreateDonorOfferPage() {
});

try {
await apiClient.post("/api/donorOffers/create", {
await apiClient.post("/api/donorOffers", {
body: formData,
});
setIsSuccess(true);
Expand Down Expand Up @@ -264,7 +265,9 @@ export default function CreateDonorOfferPage() {
<DonorOfferErrorModal
setErrorOpen={setIsError}
resetUpload={resetUpload}
errors={["An error occurred while creating the donor offer. Please try again."]}
errors={[
"An error occurred while creating the donor offer. Please try again.",
]}
/>
)}
</div>
Expand Down
3 changes: 3 additions & 0 deletions src/components/DonorOffers/PreviewTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export type DonorOfferItem = {
hfhShippingNumber?: string;
visible?: boolean;
notes?: string;
description?: string;
};

interface PreviewTableProps {
Expand Down Expand Up @@ -46,6 +47,7 @@ export const PreviewTable = ({ data, final }: PreviewTableProps) => (
"Visibility",
"Comment",
]),
"Description",
]}
rows={data.map((item) => ({
cells: [
Expand All @@ -68,6 +70,7 @@ export const PreviewTable = ({ data, final }: PreviewTableProps) => (
item.visible ? "Visible" : "Hidden",
item.notes,
]),
item.description,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style: description can be undefined - should display fallback like 'N/A' or 'Generating...' instead of blank cell

],
}))}
/>
Expand Down
108 changes: 60 additions & 48 deletions src/screens/DonorOffersScreens/AdminDynamicDonorOfferScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ type GeneralItemWithRequests = {
quantityPerUnit: number;
initialQuantity: number;
requestQuantity: number | null;
description?: string;
requests: {
id: number;
quantity: number;
Expand Down Expand Up @@ -120,6 +121,11 @@ export default function AdminDynamicDonorOfferScreen() {
header: "Quantity",
cell: (i) => i.initialQuantity,
},
{
id: "description",
header: "Description",
cell: (i) => i.description || "N/A",
},
{
id: "requestSummary",
header: "Request Summary",
Expand All @@ -132,7 +138,9 @@ export default function AdminDynamicDonorOfferScreen() {
);
return (
<div className="flex items-center gap-2">
<span className="text-gray-600">{isOpen ? <CgChevronUp /> : <CgChevronDown />}</span>
<span className="text-gray-600">
{isOpen ? <CgChevronUp /> : <CgChevronDown />}
</span>
<button className="px-2 py-1 border-black border rounded text-sm">
Total <span className="font-semibold">{totalQty}</span>
</button>
Expand Down Expand Up @@ -232,7 +240,6 @@ export default function AdminDynamicDonorOfferScreen() {

// Handle streaming chunk
if ("itemIndex" in chunk && "requests" in chunk) {

const currentItem = currentItems[chunk.itemIndex];
if (currentItem) {
updateItemRequests(currentItem.id, chunk.requests);
Expand All @@ -250,11 +257,11 @@ export default function AdminDynamicDonorOfferScreen() {
body: JSON.stringify({ donorOfferId: parseInt(String(donorOfferId)) }),
parseChunk: (rawChunk: string) => {
buffer += rawChunk;

const parts = buffer.split("\n\n");

buffer = parts.pop() || "";

for (const part of parts) {
if (part.trim().startsWith("data: ")) {
const data = part.substring(part.indexOf("data: ") + 6).trim();
Expand All @@ -268,7 +275,7 @@ export default function AdminDynamicDonorOfferScreen() {
}
}
}

return pendingMessages.shift() || ({ test: "" } as StreamChunk);
},
onChunk: (chunk) => {
Expand Down Expand Up @@ -359,51 +366,56 @@ export default function AdminDynamicDonorOfferScreen() {
}
}, [apiClient, currentItems]);

const handleRequestUpdated = useCallback((itemId?: number, updatedRequests?: PartnerRequestChipData[]) => {
if (isLLMMode && itemId && updatedRequests) {
// In LLM mode, update the specific item without reloading
tableRef.current?.updateItemById(itemId, (currentItem) => {
return {
...currentItem,
requests: currentItem.requests.map((req) => {
const updatedReq = updatedRequests.find((r) => r.id === req.id);
if (updatedReq) {
const handleRequestUpdated = useCallback(
(itemId?: number, updatedRequests?: PartnerRequestChipData[]) => {
if (isLLMMode && itemId && updatedRequests) {
// In LLM mode, update the specific item without reloading
tableRef.current?.updateItemById(itemId, (currentItem) => {
return {
...currentItem,
requests: currentItem.requests.map((req) => {
const updatedReq = updatedRequests.find((r) => r.id === req.id);
if (updatedReq) {
return {
...req,
finalQuantity: updatedReq.finalQuantity,
};
}
return req;
}),
};
});

// Also update currentItems state
setCurrentItems((prevItems) =>
prevItems.map((item) => {
if (item.id === itemId) {
return {
...req,
finalQuantity: updatedReq.finalQuantity,
...item,
requests: item.requests.map((req) => {
const updatedReq = updatedRequests.find(
(r) => r.id === req.id
);
if (updatedReq) {
return {
...req,
finalQuantity: updatedReq.finalQuantity,
};
}
return req;
}),
};
}
return req;
}),
};
});

// Also update currentItems state
setCurrentItems((prevItems) =>
prevItems.map((item) => {
if (item.id === itemId) {
return {
...item,
requests: item.requests.map((req) => {
const updatedReq = updatedRequests.find((r) => r.id === req.id);
if (updatedReq) {
return {
...req,
finalQuantity: updatedReq.finalQuantity,
};
}
return req;
}),
};
}
return item;
})
);
} else {
// Not in LLM mode, reload the table
tableRef.current?.reload();
}
}, [isLLMMode]);
return item;
})
);
} else {
// Not in LLM mode, reload the table
tableRef.current?.reload();
}
},
[isLLMMode]
);

const rowBody = useCallback(
(item: GeneralItemWithRequests) => {
Expand Down
Loading
Loading