@@ -34,6 +34,7 @@ import { buildQueryWithPagination, buildWhereFromFilters } from "@/util/table";
3434import { Partner } from "@/components/DonorOffers" ;
3535import { NotificationService } from "./notificationService" ;
3636import { MatchingService } from "./matchingService" ;
37+ import Papa from "papaparse" ;
3738
3839const DonorOfferItemSchema = z . object ( {
3940 title : z . string ( ) . trim ( ) . min ( 1 , "Title is required" ) ,
@@ -169,6 +170,11 @@ const FinalizeDonorOfferItemSchema = z.object({
169170} ) ;
170171
171172type FinalizeDonorOfferItem = z . infer < typeof FinalizeDonorOfferItemSchema > ;
173+ type GeneralItemWithRequests = GeneralItem & {
174+ requests : ( GeneralItemRequest & {
175+ partner : { id : number ; name : string } ;
176+ } ) [ ] ;
177+ } ;
172178
173179export default class DonorOfferService {
174180 private static normalizeExpirationDate ( value : unknown ) : Date | null {
@@ -907,6 +913,80 @@ export default class DonorOfferService {
907913 } ;
908914 }
909915
916+ static async getUnfinalizedDonorOfferCsv ( donorOfferId : number ) {
917+ const { donorOffer, partners, items } = await this . getAdminDonorOfferDetails (
918+ donorOfferId ,
919+ true
920+ ) ;
921+
922+ if ( donorOffer . state !== DonorOfferState . UNFINALIZED ) {
923+ throw new ArgumentError (
924+ "Only unfinalized donor offers can be converted to CSV"
925+ ) ;
926+ }
927+
928+ const itemsWithRequests = items as GeneralItemWithRequests [ ] ;
929+ const partnerMap = new Map < number , { id : number ; name : string } > ( ) ;
930+
931+ for ( const partner of partners ) {
932+ partnerMap . set ( partner . id , { id : partner . id , name : partner . name } ) ;
933+ }
934+
935+ for ( const item of itemsWithRequests ) {
936+ for ( const request of item . requests ) {
937+ partnerMap . set ( request . partner . id , request . partner ) ;
938+ }
939+ }
940+
941+ const orderedPartners = [ ...partnerMap . values ( ) ] . sort ( ( a , b ) =>
942+ a . name . localeCompare ( b . name )
943+ ) ;
944+
945+ const rows = itemsWithRequests . map ( ( item ) => {
946+ const requestTotal = item . requests . reduce (
947+ ( sum , request ) => sum + ( request . finalQuantity ?? request . quantity ?? 0 ) ,
948+ 0
949+ ) ;
950+
951+ const row : Record < string , string | number > = {
952+ "Item Name" : item . title ,
953+ Expiration : item . expirationDate
954+ ? format ( new Date ( item . expirationDate ) , "M/d/yyyy" )
955+ : "None" ,
956+ "Unit Type" : item . unitType ,
957+ Quantity : item . initialQuantity ,
958+ "Request Total" : requestTotal ,
959+ } ;
960+
961+ for ( const partner of orderedPartners ) {
962+ const request = item . requests . find ( ( r ) => r . partner . id === partner . id ) ;
963+ row [ partner . name ] = request ?. finalQuantity ?? request ?. quantity ?? 0 ;
964+ }
965+
966+ return row ;
967+ } ) ;
968+
969+ const columns = [
970+ "Item Name" ,
971+ "Expiration" ,
972+ "Unit Type" ,
973+ "Quantity" ,
974+ "Request Total" ,
975+ ...orderedPartners . map ( ( partner ) => partner . name ) ,
976+ ] ;
977+
978+ const csv = `\uFEFF${ Papa . unparse ( rows , { columns } ) } ` ;
979+ const safeOfferName = donorOffer . offerName
980+ . replace ( / [ ^ a - z 0 - 9 ] + / gi, "-" )
981+ . replace ( / ^ - + | - + $ / g, "" )
982+ . toLowerCase ( ) ;
983+
984+ return {
985+ csv,
986+ fileName : `${ safeOfferName || "donor-offer" } -requests.csv` ,
987+ } ;
988+ }
989+
910990 static async createDonorOfferRequests (
911991 requests : DonorOfferItemsRequestsDTO [ ] ,
912992 partnerId : number
0 commit comments