From 6464b904a2f136ae0a0d524425329aff6c141a9d Mon Sep 17 00:00:00 2001 From: Dalton Burkhart Date: Mon, 9 Mar 2026 03:23:02 -0400 Subject: [PATCH 1/5] Final commit --- .../components/forms/newDonationFormModal.tsx | 78 +++++++++++++++---- 1 file changed, 65 insertions(+), 13 deletions(-) diff --git a/apps/frontend/src/components/forms/newDonationFormModal.tsx b/apps/frontend/src/components/forms/newDonationFormModal.tsx index 91ad1472f..ca1aa9942 100644 --- a/apps/frontend/src/components/forms/newDonationFormModal.tsx +++ b/apps/frontend/src/components/forms/newDonationFormModal.tsx @@ -417,7 +417,11 @@ const NewDonationFormModal: React.FC = ({ } > {FoodTypes.map((type) => ( - ))} @@ -433,24 +437,39 @@ const NewDonationFormModal: React.FC = ({ placeholder="Enter #" type="number" min={1} + step={1} value={row.numItems} onChange={(e) => handleChange(row.id, 'numItems', e.target.value) } + onBlur={(e) => { + const value = Math.max( + 1, + Math.floor(Number(e.target.value) || 1), + ); + handleChange(row.id, 'numItems', String(value)); + }} /> - handleChange(row.id, 'ozPerItem', e.target.value) } + onBlur={(e) => { + const value = Math.max( + 0, + Number(e.target.value) || 0, + ); + handleChange(row.id, 'ozPerItem', String(value)); + }} /> @@ -460,7 +479,8 @@ const NewDonationFormModal: React.FC = ({ color="neutral.800" placeholder="Enter $" type="number" - min={1} + min={0} + step={0.01} value={row.valuePerItem} onChange={(e) => handleChange( @@ -469,6 +489,17 @@ const NewDonationFormModal: React.FC = ({ e.target.value, ) } + onBlur={(e) => { + const value = Math.max( + 0, + Number(e.target.value) || 0, + ); + handleChange( + row.id, + 'valuePerItem', + value.toFixed(2), + ); + }} /> @@ -517,6 +548,14 @@ const NewDonationFormModal: React.FC = ({ setRepeatEvery(e.value) } min={1} + step={1} + onBlur={() => { + const value = Math.max( + 1, + Math.floor(Number(repeatEvery) || 1), + ); + setRepeatEvery(String(value)); + }} > @@ -532,11 +571,17 @@ const NewDonationFormModal: React.FC = ({ > {(Object.values(RecurrenceEnum) as RecurrenceEnum[]) .filter((v) => v !== RecurrenceEnum.NONE) - .map((v) => ( - - ))} + .map((v) => + repeatEvery === '1' ? ( + + ) : ( + + ), + )} @@ -632,6 +677,14 @@ const NewDonationFormModal: React.FC = ({ setEndsAfter(e.value) } min={1} + step={1} + onBlur={() => { + const value = Math.max( + 1, + Math.floor(Number(endsAfter) || 1), + ); + setEndsAfter(String(value)); + }} > @@ -644,9 +697,7 @@ const NewDonationFormModal: React.FC = ({ fontSize="sm" pointerEvents="none" > - {parseInt(endsAfter) > 1 - ? 'Occurrences' - : 'Occurrence'} + {parseInt(endsAfter) > 1 ? 'Reminders' : 'Reminder'} @@ -657,7 +708,8 @@ const NewDonationFormModal: React.FC = ({ {(repeatInterval !== RecurrenceEnum.WEEKLY || Object.values(repeatOn).some(Boolean)) && ( - Next Donation scheduled for {getNextDonationDateDisplay()} + Next Donation reminder scheduled for{' '} + {getNextDonationDateDisplay()} )} From 810982416a431ac69c0f166d5db640da4f6bcf3c Mon Sep 17 00:00:00 2001 From: Dalton Burkhart Date: Mon, 9 Mar 2026 03:31:14 -0400 Subject: [PATCH 2/5] Final commit fr --- apps/frontend/src/components/forms/newDonationFormModal.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/frontend/src/components/forms/newDonationFormModal.tsx b/apps/frontend/src/components/forms/newDonationFormModal.tsx index ca1aa9942..a41701249 100644 --- a/apps/frontend/src/components/forms/newDonationFormModal.tsx +++ b/apps/frontend/src/components/forms/newDonationFormModal.tsx @@ -457,8 +457,8 @@ const NewDonationFormModal: React.FC = ({ color="neutral.800" placeholder="Enter #" type="number" - min={0} - step={0.1} + min={0.01} + step={0.01} value={row.ozPerItem} onChange={(e) => handleChange(row.id, 'ozPerItem', e.target.value) @@ -479,7 +479,7 @@ const NewDonationFormModal: React.FC = ({ color="neutral.800" placeholder="Enter $" type="number" - min={0} + min={0.01} step={0.01} value={row.valuePerItem} onChange={(e) => From 2f7f31d271e4f58813507f9257fc52207d1c2cb1 Mon Sep 17 00:00:00 2001 From: Dalton Burkhart Date: Wed, 11 Mar 2026 02:55:30 -0400 Subject: [PATCH 3/5] Sam comments --- .../frontend/src/components/forms/newDonationFormModal.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/apps/frontend/src/components/forms/newDonationFormModal.tsx b/apps/frontend/src/components/forms/newDonationFormModal.tsx index a41701249..d4a4326a0 100644 --- a/apps/frontend/src/components/forms/newDonationFormModal.tsx +++ b/apps/frontend/src/components/forms/newDonationFormModal.tsx @@ -420,7 +420,9 @@ const NewDonationFormModal: React.FC = ({ @@ -443,6 +445,7 @@ const NewDonationFormModal: React.FC = ({ handleChange(row.id, 'numItems', e.target.value) } onBlur={(e) => { + if (!e.target.value) return; const value = Math.max( 1, Math.floor(Number(e.target.value) || 1), @@ -464,6 +467,7 @@ const NewDonationFormModal: React.FC = ({ handleChange(row.id, 'ozPerItem', e.target.value) } onBlur={(e) => { + if (!e.target.value) return; const value = Math.max( 0, Number(e.target.value) || 0, @@ -490,6 +494,7 @@ const NewDonationFormModal: React.FC = ({ ) } onBlur={(e) => { + if (!e.target.value) return; const value = Math.max( 0, Number(e.target.value) || 0, From 94e5dde6b7b8fb45a4e268c41859de4e82a6ff0a Mon Sep 17 00:00:00 2001 From: Dalton Burkhart Date: Fri, 13 Mar 2026 19:02:03 -0400 Subject: [PATCH 4/5] Fixed backend validation, awaitinng merging of 148 --- .../dtos/create-donation-items.dto.ts | 23 +++++++++----- .../components/forms/newDonationFormModal.tsx | 31 ++----------------- 2 files changed, 18 insertions(+), 36 deletions(-) diff --git a/apps/backend/src/donationItems/dtos/create-donation-items.dto.ts b/apps/backend/src/donationItems/dtos/create-donation-items.dto.ts index b40d5a5ea..6c14a688d 100644 --- a/apps/backend/src/donationItems/dtos/create-donation-items.dto.ts +++ b/apps/backend/src/donationItems/dtos/create-donation-items.dto.ts @@ -10,7 +10,7 @@ import { IsOptional, IsInt, } from 'class-validator'; -import { Type } from 'class-transformer'; +import { Transform, Type } from 'class-transformer'; import { FoodType } from '../types'; export class CreateDonationItemDto { @@ -19,21 +19,30 @@ export class CreateDonationItemDto { @Length(1, 255) itemName!: string; - @IsInt() - @Min(1) + @Transform(({ value }) => parseInt(value, 10)) + @IsInt({ message: 'Quantity must be an integer value' }) + @Min(1, { message: 'Quantity must be at least 1' }) quantity!: number; @IsInt() @Min(0) reservedQuantity!: number; - @IsNumber() - @Min(0.01) + @Transform(({ value }) => parseFloat(value)) + @IsNumber( + { maxDecimalPlaces: 2 }, + { message: 'Oz per item must have at most 2 decimal places' }, + ) + @Min(0.01, { message: 'Oz per item must be at least 0.01' }) @IsOptional() ozPerItem?: number; - @IsNumber() - @Min(0.01) + @Transform(({ value }) => parseFloat(value)) + @IsNumber( + { maxDecimalPlaces: 2 }, + { message: 'Estimated value must have at most 2 decimal places' }, + ) + @Min(0.01, { message: 'Estimated value must be at least 0.01' }) @IsOptional() estimatedValue?: number; diff --git a/apps/frontend/src/components/forms/newDonationFormModal.tsx b/apps/frontend/src/components/forms/newDonationFormModal.tsx index 32f199a6a..160f2bc70 100644 --- a/apps/frontend/src/components/forms/newDonationFormModal.tsx +++ b/apps/frontend/src/components/forms/newDonationFormModal.tsx @@ -444,16 +444,9 @@ const NewDonationFormModal: React.FC = ({ onChange={(e) => handleChange(row.id, 'numItems', e.target.value) } - onBlur={(e) => { - if (!e.target.value) return; - const value = Math.max( - 1, - Math.floor(Number(e.target.value) || 1), - ); - handleChange(row.id, 'numItems', String(value)); - }} /> + = ({ onChange={(e) => handleChange(row.id, 'ozPerItem', e.target.value) } - onBlur={(e) => { - if (!e.target.value) return; - const value = Math.max( - 0.01, - Number(e.target.value) || 0.01, - ); - handleChange(row.id, 'ozPerItem', String(value)); - }} /> @@ -493,18 +478,6 @@ const NewDonationFormModal: React.FC = ({ e.target.value, ) } - onBlur={(e) => { - if (!e.target.value) return; - const value = Math.max( - 0.01, - Number(e.target.value) || 0.01, - ); - handleChange( - row.id, - 'valuePerItem', - value.toFixed(2), - ); - }} /> @@ -713,7 +686,7 @@ const NewDonationFormModal: React.FC = ({ {(repeatInterval !== RecurrenceEnum.WEEKLY || Object.values(repeatOn).some(Boolean)) && ( - Next Donation reminder scheduled for{' '} + Next donation reminder scheduled for{' '} {getNextDonationDateDisplay()} )} From a3a6072d0493ea5531561005d1f98a970292be10 Mon Sep 17 00:00:00 2001 From: Dalton Burkhart Date: Sat, 14 Mar 2026 22:39:54 -0400 Subject: [PATCH 5/5] Adjusted alerting for bad inputs --- .../components/forms/newDonationFormModal.tsx | 73 +++++++++++++------ 1 file changed, 49 insertions(+), 24 deletions(-) diff --git a/apps/frontend/src/components/forms/newDonationFormModal.tsx b/apps/frontend/src/components/forms/newDonationFormModal.tsx index 1f838640f..d2b2fc97c 100644 --- a/apps/frontend/src/components/forms/newDonationFormModal.tsx +++ b/apps/frontend/src/components/forms/newDonationFormModal.tsx @@ -53,6 +53,20 @@ const RECURRENCE_LABELS: Record = { [RecurrenceEnum.YEARLY]: 'Year', }; +// Max 2 decimal places, positive +const isValidDecimal = (val: string): boolean => + val !== '' && /^\d+(\.\d{1,2})?$/.test(val) && parseFloat(val) > 0; + +// Positive integer only +const isValidPositiveInt = (val: string): boolean => + val !== '' && /^\d+$/.test(val) && parseInt(val) > 0; + +const isRequiredFieldsFilled = (rows: DonationRow[]): boolean => + rows.every( + (row) => + row.foodItem.trim() !== '' && row.foodType !== '' && row.numItems !== '', + ); + const NewDonationFormModal: React.FC = ({ onDonationSuccess, isOpen, @@ -86,9 +100,6 @@ const NewDonationFormModal: React.FC = ({ }); const [endsAfter, setEndsAfter] = useState('1'); - const [totalItems, setTotalItems] = useState(0); - const [totalOz, setTotalOz] = useState(0); - const [totalValue, setTotalValue] = useState(0); const [alertState, setAlertMessage] = useAlert(); const handleChange = (id: number, field: string, value: string | boolean) => { @@ -149,15 +160,8 @@ const NewDonationFormModal: React.FC = ({ return `${selected.slice(0, 4).join(', ')} + ${selected.length - 4}`; }; - const handleSubmit = async () => { - const hasEmpty = rows.some( - (row) => !row.foodItem || !row.foodType || !row.numItems, - ); - if (hasEmpty) { - setAlertMessage('Please fill in all fields before submitting.'); - return; - } - + const validateAndSubmit = async () => { + // Recurring: weekly day selection if ( isRecurring && repeatInterval === RecurrenceEnum.WEEKLY && @@ -167,6 +171,31 @@ const NewDonationFormModal: React.FC = ({ return; } + // Per-row field validation + for (let i = 0; i < rows.length; i++) { + const row = rows[i]; + const rowLabel = rows.length > 1 ? ` (row ${i + 1})` : ''; + + if (!isValidPositiveInt(row.numItems)) { + setAlertMessage(`Quantity${rowLabel} must be a positive whole number.`); + return; + } + + if (row.ozPerItem !== '' && !isValidDecimal(row.ozPerItem)) { + setAlertMessage( + `Oz. per item${rowLabel} must be a positive number with at most 2 decimal places.`, + ); + return; + } + + if (row.valuePerItem !== '' && !isValidDecimal(row.valuePerItem)) { + setAlertMessage( + `Donation value${rowLabel} must be a positive number with at most 2 decimal places.`, + ); + return; + } + } + const donation_body = { foodManufacturerId: 1, recurrenceFreq: isRecurring ? parseInt(repeatEvery) : null, @@ -187,8 +216,10 @@ const NewDonationFormModal: React.FC = ({ itemName: row.foodItem, quantity: parseInt(row.numItems), reservedQuantity: 0, - ozPerItem: parseFloat(row.ozPerItem), - estimatedValue: parseFloat(row.valuePerItem), + ozPerItem: + row.ozPerItem !== '' ? parseFloat(row.ozPerItem) : undefined, + estimatedValue: + row.valuePerItem !== '' ? parseFloat(row.valuePerItem) : undefined, foodType: row.foodType as FoodType, foodRescue: row.foodRescue, })); @@ -218,6 +249,7 @@ const NewDonationFormModal: React.FC = ({ } }; + const isSubmitDisabled = !isRequiredFieldsFilled(rows); const isRepeatOnDisabled = repeatInterval !== RecurrenceEnum.WEEKLY; const placeholderStyles = { @@ -422,9 +454,6 @@ const NewDonationFormModal: React.FC = ({ _placeholder={placeholderStyles} color="neutral.800" placeholder="Enter #" - type="number" - min={1} - step={1} value={row.numItems} onChange={(e) => handleChange(row.id, 'numItems', e.target.value) @@ -437,9 +466,6 @@ const NewDonationFormModal: React.FC = ({ _placeholder={placeholderStyles} color="neutral.800" placeholder="Enter #" - type="number" - min={0.01} - step={0.01} value={row.ozPerItem} onChange={(e) => handleChange(row.id, 'ozPerItem', e.target.value) @@ -452,9 +478,6 @@ const NewDonationFormModal: React.FC = ({ _placeholder={placeholderStyles} color="neutral.800" placeholder="Enter $" - type="number" - min={0.01} - step={0.01} value={row.valuePerItem} onChange={(e) => handleChange( @@ -690,9 +713,11 @@ const NewDonationFormModal: React.FC = ({