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 && (
- setSelectedActionRequest(request)}
- >
- Complete Required Action
-
- )}
+ {formatDate(request.requestedAt)}
- )}
-
- ))}
+ {enableVolunteerActions && (
+
+ {request.status === FoodRequestStatus.ACTIVE && (
+ setSelectedActionRequest(request)}
+ >
+ Complete Required Action
+
+ )}
+
+ )}
+
+ ))}
- {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.{' '}
-
-
-
-
{
-
-
-
-
-
- By submitting this form, you agree to receive automated emails
- from Securing Safe Food (SSF) Corp. should your pantry be
- enrolled in our program.
-
-
-
void;
onSuccess: () => void;
donations: DonationDetails[];
- foodManufacturerId: number;
initialDonationId?: number | null;
onSelect: (donationId: number) => void;
}
@@ -46,7 +45,6 @@ const ResubmitDonationModal: React.FC = ({
onClose,
onSuccess,
donations,
- foodManufacturerId,
initialDonationId,
onSelect,
}) => {
diff --git a/apps/frontend/src/containers/adminDashboard.tsx b/apps/frontend/src/containers/adminDashboard.tsx
index eca2e7e8e..e2337dfbe 100644
--- a/apps/frontend/src/containers/adminDashboard.tsx
+++ b/apps/frontend/src/containers/adminDashboard.tsx
@@ -176,8 +176,8 @@ const AdminDashboard: React.FC = () => {
onLinkClick={() => {
navigate(
application.type === 'pantry'
- ? ROUTES.PANTRY_MANAGEMENT_DETAILS.replace(
- ':pantryId',
+ ? ROUTES.PANTRY_APPLICATION_DETAILS.replace(
+ ':applicationId',
application.id.toString(),
)
: ROUTES.FOOD_MANUFACTURER_APPLICATION_DETAILS.replace(
diff --git a/apps/frontend/src/containers/adminDonationStats.tsx b/apps/frontend/src/containers/adminDonationStats.tsx
index 58e3bb908..91c9c8566 100644
--- a/apps/frontend/src/containers/adminDonationStats.tsx
+++ b/apps/frontend/src/containers/adminDonationStats.tsx
@@ -367,15 +367,23 @@ const AdminDonationStats: React.FC = () => {
borderRightColor="neutral.100"
width="15%"
>
- Value of Donated Food
+ Fair Market Value of Food Donation
- Shipping Cost/Tax
+ Shipping/ Delivery Expenses
+
+
+ Shipping Paid by SSF
{
>
Total Value
-
- % of Food Rescue
+
+ % Food Rescue
+
+
+ Lbs Food Rescue
@@ -441,6 +457,14 @@ const AdminDonationStats: React.FC = () => {
>
${(totalStats?.totalShippingCost ?? 0).toFixed(2)}
+
+ ${(totalStats?.totalShippingCostPaidBySsf ?? 0).toFixed(2)}
+
{
>
${(totalStats?.totalValue ?? 0).toFixed(2)}
-
+
{(totalStats?.percentageFoodRescueItems ?? 0).toFixed(2)}%
+
+ {(totalStats?.foodRescueLbs ?? 0).toFixed(2)}
+
)}
{pantryStats.map((stat) => (
@@ -499,6 +531,13 @@ const AdminDonationStats: React.FC = () => {
>
${stat.totalShippingCost.toFixed(2)}
+
+ ${stat.totalShippingCostPaidBySsf.toFixed(2)}
+
{
>
${stat.totalValue.toFixed(2)}
-
+
{stat.percentageFoodRescueItems.toFixed(2)}%
+
+ {stat.foodRescueLbs.toFixed(2)}
+
))}
diff --git a/apps/frontend/src/containers/foodManufacturerDonationManagement.tsx b/apps/frontend/src/containers/foodManufacturerDonationManagement.tsx
index 3d36aa0ef..c5165fbb4 100644
--- a/apps/frontend/src/containers/foodManufacturerDonationManagement.tsx
+++ b/apps/frontend/src/containers/foodManufacturerDonationManagement.tsx
@@ -248,7 +248,6 @@ const FoodManufacturerDonationManagement: React.FC = () => {
onClose={handleResubmitClose}
onSuccess={() => fetchDonations()}
donations={Object.values(statusDonations).flat()}
- foodManufacturerId={manufacturerId}
initialDonationId={
resubmitDonationId ? parseInt(resubmitDonationId, 10) : null
}
diff --git a/apps/frontend/src/containers/loginPage.tsx b/apps/frontend/src/containers/loginPage.tsx
index a92fa73d5..dfcd36602 100644
--- a/apps/frontend/src/containers/loginPage.tsx
+++ b/apps/frontend/src/containers/loginPage.tsx
@@ -314,11 +314,18 @@ const LoginPage: React.FC = () => {
{step === 'login' && (
<>
-
+
Don't have an account?{' '}
navigate(ROUTES.SIGNUP)}
variant="underline"
textDecorationColor="neutral.300"
diff --git a/apps/frontend/src/containers/signupPage.tsx b/apps/frontend/src/containers/signupPage.tsx
index 745cebcdf..1374151d3 100644
--- a/apps/frontend/src/containers/signupPage.tsx
+++ b/apps/frontend/src/containers/signupPage.tsx
@@ -72,11 +72,18 @@ const SignupPage: React.FC = () => {
Food Manufacturer (Donor) Partner
-
+
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;
}[];
}