diff --git a/apps/backend/src/config/migrations.ts b/apps/backend/src/config/migrations.ts index 22819c1f7..9d62988f3 100644 --- a/apps/backend/src/config/migrations.ts +++ b/apps/backend/src/config/migrations.ts @@ -46,6 +46,7 @@ import { UpdateFoodRequestTypesAndOrder1781476891610 } from '../migrations/17814 import { UpdatePantryFMApplicationInfo1780913024514 } from '../migrations/1780913024514-UpdatePantryFMApplicationInfo'; import { AddUserActiveField1780531200000 } from '../migrations/1780531200000-AddUserActiveField'; import { MakeDonationItemDetailsRequired1782000000000 } from '../migrations/1782000000000-MakeDonationItemDetailsRequired'; +import { AddShippingCostPaidBySsf1782613537327 } from '../migrations/1782613537327-AddShippingCostPaidBySsf'; const schemaMigrations = [ User1725726359198, @@ -96,6 +97,7 @@ const schemaMigrations = [ UpdatePantryFMApplicationInfo1780913024514, AddUserActiveField1780531200000, MakeDonationItemDetailsRequired1782000000000, + AddShippingCostPaidBySsf1782613537327, ]; export default schemaMigrations; diff --git a/apps/backend/src/donations/donations.controller.ts b/apps/backend/src/donations/donations.controller.ts index 8fec63c4d..b323a68fe 100644 --- a/apps/backend/src/donations/donations.controller.ts +++ b/apps/backend/src/donations/donations.controller.ts @@ -25,8 +25,6 @@ import { OwnerIdResolver, pipeNullable, } from '../auth/ownership.decorator'; -import { FoodManufacturersService } from '../foodManufacturers/manufacturers.service'; -import { FoodManufacturer } from '../foodManufacturers/manufacturers.entity'; import { AuthenticatedRequest } from '../auth/authenticated-request'; const resolveDonationAuthorizedUserIds: OwnerIdResolver = ({ @@ -40,19 +38,6 @@ const resolveDonationAuthorizedUserIds: OwnerIdResolver = ({ ], ); -// For creating a donation, the foodManufacturerId comes from the request body -// and the only authorized non-admin caller is the manufacturer representative. -const resolveCreateDonationAuthorizedUserIds: OwnerIdResolver = ({ - entityId, - services, -}) => - pipeNullable( - () => services.get(FoodManufacturersService).findOne(entityId), - (manufacturer: FoodManufacturer) => [ - manufacturer.foodManufacturerRepresentative.id, - ], - ); - @Controller('donations') export class DonationsController { constructor(private donationService: DonationService) {} diff --git a/apps/backend/src/emails/emailTemplates.ts b/apps/backend/src/emails/emailTemplates.ts index b1d2e1f2c..261c4ed23 100644 --- a/apps/backend/src/emails/emailTemplates.ts +++ b/apps/backend/src/emails/emailTemplates.ts @@ -219,6 +219,10 @@ export const emailTemplates = { .map((item) => `
  • ${item.quantity} of ${item.product}
  • `) .join('')} +

    + We strongly recommend that clients read all ingredients and allergen labels before consuming any foods. + Any questions or issues should be directed to the manufacturer(s) and/or their healthcare provider(s). +

    To view full order details, delivery updates, and any notes from the coordinating volunteer or food manufacturer, please log in to the platform.

    diff --git a/apps/backend/src/foodManufacturers/dtos/donation-details-dto.ts b/apps/backend/src/foodManufacturers/dtos/donation-details-dto.ts index 0b11f304e..163803e7b 100644 --- a/apps/backend/src/foodManufacturers/dtos/donation-details-dto.ts +++ b/apps/backend/src/foodManufacturers/dtos/donation-details-dto.ts @@ -24,6 +24,7 @@ export class DonationOrderDetailsDto { pantryName!: string; trackingLink!: string | null; shippingCost!: number | null; + shippingCostPaidBySsf!: boolean; items!: PendingOrderItemDto[]; } diff --git a/apps/backend/src/foodManufacturers/manufacturers.controller.spec.ts b/apps/backend/src/foodManufacturers/manufacturers.controller.spec.ts index d9403a753..45f0eaaf1 100644 --- a/apps/backend/src/foodManufacturers/manufacturers.controller.spec.ts +++ b/apps/backend/src/foodManufacturers/manufacturers.controller.spec.ts @@ -145,6 +145,7 @@ describe('FoodManufacturersController', () => { pantryName: 'Community Food Pantry', trackingLink: null, shippingCost: null, + shippingCostPaidBySsf: false, items: [], }, ], diff --git a/apps/backend/src/foodManufacturers/manufacturers.service.ts b/apps/backend/src/foodManufacturers/manufacturers.service.ts index aba6fa089..c20b27697 100644 --- a/apps/backend/src/foodManufacturers/manufacturers.service.ts +++ b/apps/backend/src/foodManufacturers/manufacturers.service.ts @@ -133,6 +133,7 @@ export class FoodManufacturersService { pantryName: order.request.pantry.pantryName, trackingLink: order.trackingLink, shippingCost: order.shippingCost, + shippingCostPaidBySsf: order.shippingCostPaidBySsf, items: [], }); } diff --git a/apps/backend/src/migrations/1782613537327-AddShippingCostPaidBySsf.ts b/apps/backend/src/migrations/1782613537327-AddShippingCostPaidBySsf.ts new file mode 100644 index 000000000..9f2c59dfa --- /dev/null +++ b/apps/backend/src/migrations/1782613537327-AddShippingCostPaidBySsf.ts @@ -0,0 +1,19 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddShippingCostPaidBySsf1782613537327 + implements MigrationInterface +{ + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + ALTER TABLE orders + ADD COLUMN shipping_cost_paid_by_ssf boolean NOT NULL DEFAULT false; + `); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + ALTER TABLE orders + DROP COLUMN shipping_cost_paid_by_ssf; + `); + } +} diff --git a/apps/backend/src/orders/dtos/bulk-update-tracking-cost.dto.ts b/apps/backend/src/orders/dtos/bulk-update-tracking-cost.dto.ts index 8f9cea09f..53f726c29 100644 --- a/apps/backend/src/orders/dtos/bulk-update-tracking-cost.dto.ts +++ b/apps/backend/src/orders/dtos/bulk-update-tracking-cost.dto.ts @@ -1,5 +1,6 @@ import { IsArray, + IsBoolean, IsInt, IsNumber, IsOptional, @@ -31,6 +32,10 @@ export class OrderTrackingCostEntryDto { ) @Min(0, { message: 'Shipping cost cannot be negative' }) shippingCost?: number; + + @IsOptional() + @IsBoolean() + shippingCostPaidBySsf?: boolean; } export class BulkUpdateTrackingCostDto { diff --git a/apps/backend/src/orders/order.entity.ts b/apps/backend/src/orders/order.entity.ts index 9a891507d..396d72bf0 100644 --- a/apps/backend/src/orders/order.entity.ts +++ b/apps/backend/src/orders/order.entity.ts @@ -101,6 +101,13 @@ export class Order { }) shippingCost!: number | null; + @Column({ + name: 'shipping_cost_paid_by_ssf', + type: 'boolean', + default: false, + }) + shippingCostPaidBySsf!: boolean; + @ManyToOne(() => User, { nullable: false, onDelete: 'RESTRICT' }) @JoinColumn({ name: 'assignee_id' }) assignee!: User; diff --git a/apps/backend/src/orders/order.service.spec.ts b/apps/backend/src/orders/order.service.spec.ts index d6724d7b4..baa122704 100644 --- a/apps/backend/src/orders/order.service.spec.ts +++ b/apps/backend/src/orders/order.service.spec.ts @@ -1942,6 +1942,30 @@ ${request.pantry.shipmentAddressCity}, ${request.pantry.shipmentAddressState} ${ expect(after2.shippedAt).toBeDefined(); }); + it('persists the shippingCostPaidBySsf flag when provided', async () => { + const donationId = await insertMatchedDonation(); + const itemId = await insertDonationItem(donationId); + await insertAllocation(4, itemId); + + const before = await service.findOne(4); + expect(before.shippingCostPaidBySsf).toBe(false); + + await service.bulkUpdateTrackingCostInfo({ + donationId, + orders: [ + { + orderId: 4, + shippingCost: 5.0, + shippingCostPaidBySsf: true, + }, + ], + }); + + const after = await service.findOne(4); + expect(after.shippingCost).toEqual(5.0); + expect(after.shippingCostPaidBySsf).toBe(true); + }); + it('updates only tracking link when no shipping cost is provided, order stays PENDING', async () => { const donationId = await insertMatchedDonation(); const itemId = await insertDonationItem(donationId); diff --git a/apps/backend/src/orders/order.service.ts b/apps/backend/src/orders/order.service.ts index ab9ab8434..61ad6603f 100644 --- a/apps/backend/src/orders/order.service.ts +++ b/apps/backend/src/orders/order.service.ts @@ -752,6 +752,9 @@ ${request.pantry.shipmentAddressCity}, ${request.pantry.shipmentAddressState} ${ if (entry.shippingCost !== undefined) { order.shippingCost = entry.shippingCost; } + if (entry.shippingCostPaidBySsf !== undefined) { + order.shippingCostPaidBySsf = entry.shippingCostPaidBySsf; + } if (order.trackingLink !== null && order.shippingCost !== null) { order.status = OrderStatus.SHIPPED; order.shippedAt = new Date(); diff --git a/apps/backend/src/pantries/pantries.controller.spec.ts b/apps/backend/src/pantries/pantries.controller.spec.ts index c55aa1fa3..f25483681 100644 --- a/apps/backend/src/pantries/pantries.controller.spec.ts +++ b/apps/backend/src/pantries/pantries.controller.spec.ts @@ -467,8 +467,10 @@ describe('PantriesController', () => { totalLbs: 100, totalDonatedFoodValue: 500, totalShippingCost: 50, + totalShippingCostPaidBySsf: 20, totalValue: 550, - percentageFoodRescueItems: 80, + percentageFoodRescueItems: 40, + foodRescueLbs: 80, }, ]; @@ -507,8 +509,10 @@ describe('PantriesController', () => { totalLbs: 500, totalDonatedFoodValue: 2500, totalShippingCost: 200, + totalShippingCostPaidBySsf: 80, totalValue: 2700, - percentageFoodRescueItems: 75, + percentageFoodRescueItems: 30, + foodRescueLbs: 75, }; mockPantriesService.getTotalStats.mockResolvedValueOnce(mockTotalStats); @@ -526,8 +530,10 @@ describe('PantriesController', () => { totalLbs: 500, totalDonatedFoodValue: 2500, totalShippingCost: 200, + totalShippingCostPaidBySsf: 80, totalValue: 2700, - percentageFoodRescueItems: 75, + percentageFoodRescueItems: 30, + foodRescueLbs: 75, }; mockPantriesService.getTotalStats.mockResolvedValueOnce(mockTotalStats); diff --git a/apps/backend/src/pantries/pantries.service.spec.ts b/apps/backend/src/pantries/pantries.service.spec.ts index c73969c0f..3b247eb09 100644 --- a/apps/backend/src/pantries/pantries.service.spec.ts +++ b/apps/backend/src/pantries/pantries.service.spec.ts @@ -529,6 +529,34 @@ describe('PantriesService', () => { expect(stats.totalDonatedFoodValue).toBeCloseTo(625.0, 2); expect(stats.totalValue).toBeCloseTo(645.0, 2); expect(stats.percentageFoodRescueItems).toBe(0); + expect(stats.foodRescueLbs).toBe(0); + // No orders are flagged SSF-paid in the seed data + expect(stats.totalShippingCostPaidBySsf).toBe(0); + }); + + it('only sums SSF-paid orders into totalShippingCostPaidBySsf', async () => { + // Capture the pantry's shipping before flagging anything as SSF-paid. + const before = ( + await service.getPantryStats(['Community Food Pantry Downtown']) + )[0]; + expect(before.totalShippingCost).toBeGreaterThan(0); + + // Flag every order as paid by SSF. SSF-paid shipping is excluded from + // totalShippingCost, so that total should drop to zero while the SSF-paid + // total picks up the entire shipping amount. + await testDataSource.query( + `UPDATE public.orders SET shipping_cost_paid_by_ssf = true`, + ); + + const stats = ( + await service.getPantryStats(['Community Food Pantry Downtown']) + )[0]; + + expect(stats.totalShippingCost).toBe(0); + expect(stats.totalShippingCostPaidBySsf).toBeCloseTo( + before.totalShippingCost, + 2, + ); }); it('throws NotFoundException for a non-approved (denied) pantry', async () => { @@ -567,6 +595,7 @@ describe('PantriesService', () => { expect(stats.totalShippingCost).toBe(0); expect(stats.totalValue).toBe(0); expect(stats.percentageFoodRescueItems).toBe(0); + expect(stats.foodRescueLbs).toBe(0); }); it('respects year filter and returns zeros for a non-matching year', async () => { @@ -582,6 +611,7 @@ describe('PantriesService', () => { expect(stats.totalShippingCost).toBe(0); expect(stats.totalValue).toBe(0); expect(stats.percentageFoodRescueItems).toBe(0); + expect(stats.foodRescueLbs).toBe(0); }); }); @@ -817,6 +847,7 @@ describe('PantriesService', () => { expect(totalEmpty.totalShippingCost).toBe(0); expect(totalEmpty.totalValue).toBe(0); expect(totalEmpty.percentageFoodRescueItems).toBe(0); + expect(totalEmpty.foodRescueLbs).toBe(0); }); it('returns all zeros when no approved pantries exist', async () => { @@ -831,6 +862,7 @@ describe('PantriesService', () => { expect(total.totalShippingCost).toBe(0); expect(total.totalValue).toBe(0); expect(total.percentageFoodRescueItems).toBe(0); + expect(total.foodRescueLbs).toBe(0); }); it('excludes a pantry set to pending from the totals', async () => { diff --git a/apps/backend/src/pantries/pantries.service.ts b/apps/backend/src/pantries/pantries.service.ts index 36d341e83..16b7a0acd 100644 --- a/apps/backend/src/pantries/pantries.service.ts +++ b/apps/backend/src/pantries/pantries.service.ts @@ -65,15 +65,17 @@ export class PantriesService { totalLbs: 0, totalDonatedFoodValue: 0, totalShippingCost: 0, + totalShippingCostPaidBySsf: 0, totalValue: 0, percentageFoodRescueItems: 0, + foodRescueLbs: 0, }; private async aggregateStats( pantryIds: number[], years?: number[], ): Promise[]> { - // Query 1: aggregate item stats (totalItems, totalOz, totalDonatedFoodValue, totalFoodRescueItems) + // Query 1: aggregate item stats (totalItems, totalOz, totalDonatedFoodValue, foodRescueItems, foodRescueOz) const itemsQb = this.orderRepo .createQueryBuilder('order') .leftJoin('order.request', 'request') @@ -91,7 +93,11 @@ export class PantriesService { ) .addSelect( `COALESCE(SUM(CASE WHEN item.foodRescue = true THEN allocation.allocatedQuantity ELSE 0 END), 0)`, - 'totalFoodRescueItems', + 'foodRescueItems', + ) + .addSelect( + `COALESCE(SUM(CASE WHEN item.foodRescue = true THEN COALESCE(item.ozPerItem, 0) * allocation.allocatedQuantity ELSE 0 END), 0)`, + 'foodRescueOz', ) .where('request.pantryId IN (:...pantryIds)', { pantryIds }) .groupBy('request.pantryId'); @@ -107,7 +113,14 @@ export class PantriesService { .createQueryBuilder('order') .leftJoin('order.request', 'request') .select('request.pantryId', 'pantryId') - .addSelect('COALESCE(SUM(order.shippingCost), 0)', 'totalShippingCost') + .addSelect( + `COALESCE(SUM(CASE WHEN order.shippingCostPaidBySsf = true THEN 0 ELSE order.shippingCost END), 0)`, + 'totalShippingCost', + ) + .addSelect( + `COALESCE(SUM(CASE WHEN order.shippingCostPaidBySsf = true THEN order.shippingCost ELSE 0 END), 0)`, + 'totalShippingCostPaidBySsf', + ) .where('request.pantryId IN (:...pantryIds)', { pantryIds }) .groupBy('request.pantryId'); @@ -126,7 +139,10 @@ export class PantriesService { const shippingMap = new Map( shippingRows.map((r) => [ Number(r.pantryId), - Number(r.totalShippingCost), + { + totalShippingCost: Number(r.totalShippingCost), + totalShippingCostPaidBySsf: Number(r.totalShippingCostPaidBySsf), + }, ]), ); @@ -134,8 +150,12 @@ export class PantriesService { const totalItems = Number(row.totalItems); const totalOz = Number(row.totalOz); const totalDonatedFoodValue = Number(row.totalDonatedFoodValue); - const totalShippingCost = shippingMap.get(Number(row.pantryId)) ?? 0; - const totalFoodRescueItems = Number(row.totalFoodRescueItems); + const shipping = shippingMap.get(Number(row.pantryId)); + const totalShippingCost = shipping?.totalShippingCost ?? 0; + const totalShippingCostPaidBySsf = + shipping?.totalShippingCostPaidBySsf ?? 0; + const foodRescueItems = Number(row.foodRescueItems); + const foodRescueOz = Number(row.foodRescueOz); return { pantryId: Number(row.pantryId), @@ -144,11 +164,13 @@ export class PantriesService { totalLbs: parseFloat((totalOz / 16).toFixed(2)), totalDonatedFoodValue, totalShippingCost, + totalShippingCostPaidBySsf, totalValue: totalDonatedFoodValue + totalShippingCost, percentageFoodRescueItems: totalItems > 0 - ? parseFloat(((totalFoodRescueItems / totalItems) * 100).toFixed(2)) + ? parseFloat(((foodRescueItems / totalItems) * 100).toFixed(2)) : 0, + foodRescueLbs: parseFloat((foodRescueOz / 16).toFixed(2)), } satisfies Omit; }); } @@ -242,12 +264,15 @@ export class PantriesService { totalStats.totalOz += s.totalOz; totalStats.totalDonatedFoodValue += s.totalDonatedFoodValue; totalStats.totalShippingCost += s.totalShippingCost; + totalStats.totalShippingCostPaidBySsf += s.totalShippingCostPaidBySsf; totalStats.totalValue += s.totalValue; + totalStats.foodRescueLbs += s.foodRescueLbs; totalFoodRescueItems += (s.percentageFoodRescueItems / 100) * s.totalItems; } totalStats.totalLbs = parseFloat((totalStats.totalOz / 16).toFixed(2)); + totalStats.foodRescueLbs = parseFloat(totalStats.foodRescueLbs.toFixed(2)); totalStats.percentageFoodRescueItems = totalStats.totalItems > 0 ? parseFloat( diff --git a/apps/backend/src/pantries/types.ts b/apps/backend/src/pantries/types.ts index f671dbcd7..999740901 100644 --- a/apps/backend/src/pantries/types.ts +++ b/apps/backend/src/pantries/types.ts @@ -92,8 +92,10 @@ export type PantryStats = { totalLbs: number; totalDonatedFoodValue: number; totalShippingCost: number; + totalShippingCostPaidBySsf: number; totalValue: number; percentageFoodRescueItems: number; + foodRescueLbs: number; }; // Make new type that is just a list of PantryStats with pantryId and pantryName omitted diff --git a/apps/frontend/src/components/foodRequestManagement.tsx b/apps/frontend/src/components/foodRequestManagement.tsx index e03e33d88..d7ba543d5 100644 --- a/apps/frontend/src/components/foodRequestManagement.tsx +++ b/apps/frontend/src/components/foodRequestManagement.tsx @@ -15,6 +15,8 @@ import { ArrowDownUp, ChevronRight, ChevronLeft, Funnel } from 'lucide-react'; import { capitalize, formatDate } from '@utils/utils'; import { FloatingAlert } from '@components/floatingAlert'; import { FoodRequestStatus, FoodRequestSummaryDto } from '../types/types'; +import { ROUTES } from '../routes'; +import PageEmptyState from '@components/pageEmptyState'; import RequestDetailsModal from '@components/forms/requestDetailsModal'; import PantryDeleteRequestActionModal from '@components/forms/pantryDeleteRequestModal'; import VolunteerCloseRequestActionModal from '@components/forms/volunteerCloseRequestModal'; @@ -158,6 +160,16 @@ const RequestManagement: React.FC = ({ const clearCloseRequest = () => setSelectedCloseRequestAction(null); const clearCreateOrder = () => setSelectedCreateOrderRequest(null); + const emptyStatePrimary = enableVolunteerActions + ? { + text: 'View Assigned Pantries', + link: ROUTES.VOLUNTEER_ASSIGNED_PANTRIES, + } + : { text: 'View Orders', link: ROUTES.ADMIN_ORDER_MANAGEMENT }; + const emptyStateSecondary = enableVolunteerActions + ? { text: 'View Orders', link: ROUTES.VOLUNTEER_ORDER_MANAGEMENT } + : { text: 'View Dashboard', link: ROUTES.ADMIN_DASHBOARD }; + return ( @@ -253,209 +265,219 @@ const RequestManagement: React.FC = ({ Sort - - - - - Request # - - - Status - - - Pantry - - - Date Requested - - {enableVolunteerActions && ( + {paginatedRequests.length === 0 ? ( + + ) : ( + + + - Action Required - - )} - - - - {paginatedRequests.map((request) => ( - - - setSelectedViewDetailsRequest(request)} - > - {request.requestId} - - - + - - {capitalize(request.status)} - - - + - {request.pantry.pantryName} - - + - {formatDate(request.requestedAt)} - + Date Requested + {enableVolunteerActions && ( + + Action Required + + )} + + + + {paginatedRequests.map((request) => ( + + + setSelectedViewDetailsRequest(request)} + > + {request.requestId} + + + + + {capitalize(request.status)} + + + + {request.pantry.pantryName} + - {request.status === FoodRequestStatus.ACTIVE && ( - - )} + {formatDate(request.requestedAt)} - )} - - ))} + {enableVolunteerActions && ( + + {request.status === FoodRequestStatus.ACTIVE && ( + + )} + + )} + + ))} - {selectedViewDetailsRequest && ( - { - setSelectedViewDetailsRequest(null); - if (initialRequestId) { - navigate(location.pathname, { replace: true }); - } - }} - onSuccess={loadRequests} - onDelete={() => setDeleteRequest(selectedViewDetailsRequest)} - /> - )} + {selectedViewDetailsRequest && ( + { + setSelectedViewDetailsRequest(null); + if (initialRequestId) { + navigate(location.pathname, { replace: true }); + } + }} + onSuccess={loadRequests} + onDelete={() => setDeleteRequest(selectedViewDetailsRequest)} + /> + )} - {deleteRequest && ( - setDeleteRequest(null)} - onSuccess={() => { - setAlertMessage( - 'Successfully deleted food request.', - AlertStatus.INFO, - ); - loadRequests(); - setSelectedViewDetailsRequest(null); - }} - /> - )} + {deleteRequest && ( + setDeleteRequest(null)} + onSuccess={() => { + setAlertMessage( + 'Successfully deleted food request.', + AlertStatus.INFO, + ); + loadRequests(); + setSelectedViewDetailsRequest(null); + }} + /> + )} - {selectedActionRequest && ( - { - setSelectedCloseRequestAction(selectedActionRequest); - }} - onCreateOrder={() => { - setSelectedCreateOrderRequest(selectedActionRequest); - }} - /> - )} + {selectedActionRequest && ( + { + setSelectedCloseRequestAction(selectedActionRequest); + }} + onCreateOrder={() => { + setSelectedCreateOrderRequest(selectedActionRequest); + }} + /> + )} - {selectedCloseRequestAction && ( - { - setAlertMessage('Request Closed', AlertStatus.INFO); - loadRequests(); - }} - /> - )} + {selectedCloseRequestAction && ( + { + setAlertMessage('Request Closed', AlertStatus.INFO); + loadRequests(); + }} + /> + )} - {selectedCreateOrderRequest && ( - { - setAlertMessage('Order Created', AlertStatus.INFO); - loadRequests(); - }} - /> - )} - - + {selectedCreateOrderRequest && ( + { + setAlertMessage('Order Created', AlertStatus.INFO); + loadRequests(); + }} + /> + )} + + + )} {totalPages > 1 && ( = ({ ))} )} + + {!isEditing && donation.recurrence !== RecurrenceEnum.NONE && ( + + + Donation sets up recurring reminders + + + {donation.nextDonationDates && + donation.nextDonationDates.length > 0 && ( + + + Upcoming reminder emails + + + {donation.nextDonationDates + .map((date) => formatDate(date)) + .join(', ')} + + + )} + + )} diff --git a/apps/frontend/src/components/forms/fmCompleteRequiredActionsModal.tsx b/apps/frontend/src/components/forms/fmCompleteRequiredActionsModal.tsx index 1fc65adc4..b8ad09b97 100644 --- a/apps/frontend/src/components/forms/fmCompleteRequiredActionsModal.tsx +++ b/apps/frontend/src/components/forms/fmCompleteRequiredActionsModal.tsx @@ -46,6 +46,7 @@ interface FmCompleteRequiredActionsModalProps { interface OrderFormData { shippingCost: string; trackingLink: string; + shippingCostPaidBySsf: boolean; } interface ItemFormData { @@ -137,6 +138,7 @@ const FmCompleteRequiredActionsModal: React.FC< { shippingCost: o.shippingCost?.toString() ?? '', trackingLink: o.trackingLink ?? '', + shippingCostPaidBySsf: o.shippingCostPaidBySsf, }, ]), ), @@ -179,7 +181,7 @@ const FmCompleteRequiredActionsModal: React.FC< const updateOrderField = ( orderId: number, field: keyof OrderFormData, - value: string, + value: string | boolean, ) => { setOrderFormData((prev) => ({ ...prev, @@ -255,11 +257,19 @@ const FmCompleteRequiredActionsModal: React.FC< // Only include orders where the user actually changed a value from the original prop values const ordersToUpdate = orders .filter((order) => { - const { trackingLink, shippingCost } = orderFormData[order.orderId]; + const { trackingLink, shippingCost, shippingCostPaidBySsf } = + orderFormData[order.orderId]; const originalTracking = order.trackingLink ?? ''; const originalCost = order.shippingCost?.toString() ?? ''; + // The paid-by-SSF flag is only meaningful alongside a shipping cost, + // so only treat a flag change as a change when a cost is present. + const flagChanged = + shippingCost !== '' && + shippingCostPaidBySsf !== order.shippingCostPaidBySsf; return ( - trackingLink !== originalTracking || shippingCost !== originalCost + trackingLink !== originalTracking || + shippingCost !== originalCost || + flagChanged ); }) .map( @@ -269,13 +279,16 @@ const FmCompleteRequiredActionsModal: React.FC< orderId: number; trackingLink?: string; shippingCost?: number; + shippingCostPaidBySsf?: boolean; } => { - const { trackingLink, shippingCost } = orderFormData[order.orderId]; + const { trackingLink, shippingCost, shippingCostPaidBySsf } = + orderFormData[order.orderId]; return { orderId: order.orderId, ...(trackingLink.trim() !== '' && { trackingLink }), ...(shippingCost !== '' && { shippingCost: parseFloat(shippingCost), + shippingCostPaidBySsf, }), }; }, @@ -373,6 +386,33 @@ const FmCompleteRequiredActionsModal: React.FC< ) } /> + + updateOrderField( + currentOrder.orderId, + 'shippingCostPaidBySsf', + !!e.checked, + ) + } + > + + + + + + Shipping cost paid by SSF + + diff --git a/apps/frontend/src/components/forms/manufacturerApplicationForm.tsx b/apps/frontend/src/components/forms/manufacturerApplicationForm.tsx index 34ad1bb5b..6b4c30024 100644 --- a/apps/frontend/src/components/forms/manufacturerApplicationForm.tsx +++ b/apps/frontend/src/components/forms/manufacturerApplicationForm.tsx @@ -594,22 +594,6 @@ const ManufacturerApplicationForm: React.FC = () => { - - - - - - By submitting this form, you agree to receive automated emails - from Securing Safe Food (SSF) Corp. should your food - manufacturer be enrolled in our program.{' '} - - - - - + Already have an account?{' '} navigate(ROUTES.LOGIN)} variant="underline" textDecorationColor="neutral.300" diff --git a/apps/frontend/src/types/types.ts b/apps/frontend/src/types/types.ts index b0bfdd4e8..de839fe8f 100644 --- a/apps/frontend/src/types/types.ts +++ b/apps/frontend/src/types/types.ts @@ -232,6 +232,7 @@ export interface DonationOrderDetails { pantryName: string; trackingLink: string | null; shippingCost: number | null; + shippingCostPaidBySsf: boolean; items: OrderItemDetails[]; } @@ -602,8 +603,10 @@ export interface PantryStats { totalLbs: number; totalDonatedFoodValue: number; totalShippingCost: number; + totalShippingCostPaidBySsf: number; totalValue: number; percentageFoodRescueItems: number; + foodRescueLbs: number; } export type TotalStats = Omit; @@ -624,6 +627,7 @@ export interface BulkUpdateTrackingCostDto { orderId: number; trackingLink?: string; shippingCost?: number; + shippingCostPaidBySsf?: boolean; }[]; }