Skip to content

Commit b4a316f

Browse files
authored
Merge pull request #4159 from Northeastern-Electric-Racing/#4102-sponsor-logoimageid-frontend
#4102 sponsor logoimageid frontend
2 parents 74b4fad + 16c1409 commit b4a316f

9 files changed

Lines changed: 177 additions & 26 deletions

File tree

src/backend/src/controllers/finance.controllers.ts

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { NextFunction, Request, Response } from 'express';
22
import FinanceServices from '../services/finance.services.js';
3+
import { HttpException } from '../utils/errors.utils.js';
34

45
export default class FinanceController {
56
static async createSponsor(req: Request, res: Response, next: NextFunction) {
@@ -43,8 +44,7 @@ export default class FinanceController {
4344
contactPhone,
4445
contactPosition,
4546
stockDescription,
46-
discountDescription,
47-
req.file
47+
discountDescription
4848
);
4949
res.status(200).json(sponsor);
5050
} catch (error: unknown) {
@@ -384,8 +384,7 @@ export default class FinanceController {
384384
contactPhone,
385385
contactPosition,
386386
stockDescription,
387-
discountDescription,
388-
req.file
387+
discountDescription
389388
);
390389

391390
res.status(200).json(updatedSponsor);
@@ -432,4 +431,15 @@ export default class FinanceController {
432431
next(error);
433432
}
434433
}
434+
435+
static async uploadSponsorLogo(req: Request, res: Response, next: NextFunction) {
436+
try {
437+
const { sponsorId } = req.params as Record<string, string>;
438+
if (!req.file) throw new HttpException(400, 'Invalid or undefined image data');
439+
const updatedSponsor = await FinanceServices.uploadSponsorLogo(req.currentUser, req.organization, sponsorId, req.file);
440+
res.status(200).json(updatedSponsor);
441+
} catch (error: unknown) {
442+
next(error);
443+
}
444+
}
435445
}

src/backend/src/routes/finance.routes.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ const upload = multer({ limits: { fileSize: MAX_FILE_SIZE }, storage: memoryStor
1717

1818
financeRouter.post(
1919
'/sponsor/create',
20-
upload.single('logoImage'),
2120
nonEmptyString(body('name')),
2221
body('activeStatus').isBoolean(),
2322
body('valueTypes').isArray(),
@@ -52,6 +51,13 @@ financeRouter.get('/sponsor/:sponsorId/sponsorTasks', FinanceController.getSpons
5251

5352
financeRouter.post('/sponsor/:sponsorId/delete', FinanceController.deleteSponsor);
5453

54+
financeRouter.post(
55+
'/sponsor/:sponsorId/uploadLogo',
56+
upload.single('logoImage'),
57+
validateInputs,
58+
FinanceController.uploadSponsorLogo
59+
);
60+
5561
financeRouter.post(
5662
'/sponsorTier/create',
5763
nonEmptyString(body('name')),
@@ -146,7 +152,6 @@ financeRouter.get('/sponsorTiers', FinanceController.getAllSponsorTiers);
146152

147153
financeRouter.post(
148154
'/sponsor/:sponsorId/edit',
149-
upload.single('logoImage'),
150155
nonEmptyString(body('name')),
151156
body('activeStatus').isBoolean(),
152157
body('valueTypes').isArray(),

src/backend/src/services/finance.services.ts

Lines changed: 38 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -81,8 +81,7 @@ export default class FinanceServices {
8181
contactPhone?: string,
8282
contactPosition?: string,
8383
stockDescription?: string,
84-
discountDescription?: string,
85-
logoImage?: Express.Multer.File
84+
discountDescription?: string
8685
) {
8786
if (!(await userHasPermission(submitter.userId, organization.organizationId, isHead)))
8887
throw new AccessDeniedException('Only heads can create a sponsor');
@@ -106,8 +105,6 @@ export default class FinanceServices {
106105
data: { name: contactName, email: contactEmail, phone: contactPhone, position: contactPosition }
107106
});
108107

109-
const { id: logoImageId } = logoImage ? await uploadFile(logoImage) : { id: undefined };
110-
111108
const sponsor = await prisma.sponsor.create({
112109
data: {
113110
name,
@@ -122,7 +119,6 @@ export default class FinanceServices {
122119
taxExempt,
123120
discountCode,
124121
sponsorNotes,
125-
logoImageId,
126122
contactId: contact.sponsorContactId,
127123
sponsorTasks: {
128124
create: sponsorTasks.map((task) => ({
@@ -1230,8 +1226,7 @@ export default class FinanceServices {
12301226
contactPhone?: string,
12311227
contactPosition?: string,
12321228
stockDescription?: string,
1233-
discountDescription?: string,
1234-
logoImage?: Express.Multer.File
1229+
discountDescription?: string
12351230
): Promise<Sponsor> {
12361231
if (!(await userHasPermission(submitter.userId, organization.organizationId, isHead)))
12371232
throw new AccessDeniedException('Only heads can edit sponsors.');
@@ -1329,8 +1324,6 @@ export default class FinanceServices {
13291324
data: { name: contactName, email: contactEmail, phone: contactPhone, position: contactPosition }
13301325
});
13311326

1332-
const { id: logoImageId } = logoImage ? await uploadFile(logoImage) : { id: undefined };
1333-
13341327
const updatedSponsor = await prisma.sponsor.update({
13351328
where: { sponsorId: oldSponsor.sponsorId },
13361329
data: {
@@ -1345,15 +1338,49 @@ export default class FinanceServices {
13451338
tier: sponsorTierId ? { connect: { sponsorTierId } } : { disconnect: true },
13461339
taxExempt,
13471340
discountCode,
1348-
sponsorNotes,
1349-
...(logoImageId && { logoImageId })
1341+
sponsorNotes
13501342
},
13511343
...getSponsorQueryArgs(organization.organizationId)
13521344
});
13531345

13541346
return sponsorTransformer(updatedSponsor);
13551347
}
13561348

1349+
/**
1350+
* Uploads a logo image for a sponsor and stores the resulting image ID.
1351+
*
1352+
* @param submitter The user performing the upload
1353+
* @param organization The organization the sponsor belongs to
1354+
* @param sponsorId The id of the sponsor
1355+
* @param logoImage The logo image file to upload
1356+
* @returns The updated sponsor
1357+
*/
1358+
static async uploadSponsorLogo(
1359+
submitter: User,
1360+
organization: Organization,
1361+
sponsorId: string,
1362+
logoImage: Express.Multer.File
1363+
): Promise<Sponsor> {
1364+
if (!(await userHasPermission(submitter.userId, organization.organizationId, isHead)))
1365+
throw new AccessDeniedException('Only heads can update a sponsor logo');
1366+
1367+
const sponsor = await prisma.sponsor.findUnique({
1368+
where: { sponsorId, organizationId: organization.organizationId }
1369+
});
1370+
1371+
if (!sponsor) throw new NotFoundException('Sponsor', sponsorId);
1372+
1373+
const { id } = await uploadFile(logoImage);
1374+
1375+
const updatedSponsor = await prisma.sponsor.update({
1376+
where: { sponsorId },
1377+
data: { logoImageId: id },
1378+
...getSponsorQueryArgs(organization.organizationId)
1379+
});
1380+
1381+
return sponsorTransformer(updatedSponsor);
1382+
}
1383+
13571384
/**
13581385
* Toggles the done status of a sponsor task
13591386
* @param submitter The user toggling the task

src/frontend/src/apis/finance.api.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -742,6 +742,12 @@ export const editSponsor = (id: string, formData: SponsorPayload) => {
742742
return axios.post(apiUrls.editSponsor(id), formData);
743743
};
744744

745+
export const uploadSponsorLogo = (sponsorId: string, logoImage: File) => {
746+
const formData = new FormData();
747+
formData.append('logoImage', logoImage);
748+
return axios.post<Sponsor>(apiUrls.uploadSponsorLogo(sponsorId), formData);
749+
};
750+
745751
/**
746752
* API call to delete a given sponsor task
747753
*

src/frontend/src/hooks/finance.hooks.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ import {
6969
assignMemberToRR,
7070
setTaxExemptStatus,
7171
toggleSponsorTaskDone,
72+
uploadSponsorLogo,
7273
getAllProspectiveSponsors,
7374
createProspectiveSponsor,
7475
editProspectiveSponsor,
@@ -186,6 +187,7 @@ export interface SponsorPayload {
186187
sponsorNotes?: string;
187188
stockDescription?: string;
188189
discountDescription?: string;
190+
logoImageId?: string;
189191
}
190192

191193
interface EditSponsorPayload extends SponsorPayload {
@@ -1324,6 +1326,27 @@ export const useEditSponsor = () => {
13241326
);
13251327
};
13261328

1329+
interface UploadSponsorLogoPayload {
1330+
sponsorId: string;
1331+
logoImage: File;
1332+
}
1333+
1334+
export const useUploadSponsorLogo = () => {
1335+
const queryClient = useQueryClient();
1336+
return useMutation<Sponsor, Error, UploadSponsorLogoPayload>(
1337+
['sponsor', 'uploadLogo'],
1338+
async ({ sponsorId, logoImage }: UploadSponsorLogoPayload) => {
1339+
const { data } = await uploadSponsorLogo(sponsorId, logoImage);
1340+
return data;
1341+
},
1342+
{
1343+
onSuccess: () => {
1344+
queryClient.invalidateQueries(['sponsor']);
1345+
}
1346+
}
1347+
);
1348+
};
1349+
13271350
/**
13281351
* Custom React Hook to delete a sponsor task
13291352
*

src/frontend/src/pages/FinancePage/FinanceComponents/CreateSponsorPage.tsx

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { useForm } from 'react-hook-form';
22
import LoadingIndicator from '../../../components/LoadingIndicator';
3-
import { SponsorPayload, useCreateSponsor } from '../../../hooks/finance.hooks';
3+
import { SponsorPayload, useCreateSponsor, useUploadSponsorLogo } from '../../../hooks/finance.hooks';
44
import sponsorSchema, { SponsorForm } from './SponsorForm';
55
import { yupResolver } from '@hookform/resolvers/yup';
66
import { Box } from '@mui/system';
@@ -17,7 +17,8 @@ interface CreateSponsorPageProps {
1717

1818
const CreateSponsorPage = ({ showPage, handleClose }: CreateSponsorPageProps) => {
1919
const toast = useToast();
20-
const { isLoading, mutateAsync } = useCreateSponsor();
20+
const { isLoading, mutateAsync: createSponsor } = useCreateSponsor();
21+
const { mutateAsync: uploadLogo } = useUploadSponsorLogo();
2122

2223
const {
2324
handleSubmit,
@@ -48,12 +49,17 @@ const CreateSponsorPage = ({ showPage, handleClose }: CreateSponsorPageProps) =>
4849
});
4950

5051
const [submitError, setSubmitError] = useState<string | null>(null);
52+
const [logoImage, setLogoImage] = useState<File | null>(null);
53+
5154
if (isLoading) return <LoadingIndicator />;
5255

5356
const onFormSubmit = async (formData: SponsorPayload) => {
5457
try {
5558
setSubmitError(null);
56-
await mutateAsync({ ...formData });
59+
const sponsor = await createSponsor(formData);
60+
if (logoImage) {
61+
await uploadLogo({ sponsorId: sponsor.sponsorId, logoImage });
62+
}
5763
toast.success('Sponsor created successfully!');
5864
handleClose();
5965
} catch (err: unknown) {
@@ -71,7 +77,7 @@ const CreateSponsorPage = ({ showPage, handleClose }: CreateSponsorPageProps) =>
7177
title="Add Sponsor"
7278
component={
7379
<Box display="flex" flexDirection="column" alignItems="flex-end">
74-
<SponsorForm control={control} errors={errors} setValue={setValue} />
80+
<SponsorForm control={control} errors={errors} setValue={setValue} onLogoImageChange={setLogoImage} />
7581
{submitError && (
7682
<Box color="error.main" mb={2} fontWeight="bold">
7783
{submitError}

src/frontend/src/pages/FinancePage/FinanceComponents/EditSponsorPage.tsx

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { CreateSponsorTask, Sponsor } from 'shared';
22
import LoadingIndicator from '../../../components/LoadingIndicator';
3-
import { SponsorPayload, useEditSponsor } from '../../../hooks/finance.hooks';
3+
import { SponsorPayload, useEditSponsor, useUploadSponsorLogo } from '../../../hooks/finance.hooks';
44
import SidePage from './SidePagePopup';
55
import sponsorSchema, { SponsorForm } from './SponsorForm';
66

@@ -21,6 +21,7 @@ interface EditSponsorPageProps {
2121
const EditSponsorPage = ({ showPage, handleClose, sponsor }: EditSponsorPageProps) => {
2222
const toast = useToast();
2323
const { isLoading, mutateAsync } = useEditSponsor();
24+
const { mutateAsync: uploadLogo } = useUploadSponsorLogo();
2425

2526
const defaultSponsorTasks: CreateSponsorTask[] = (
2627
sponsor.sponsorTasks?.map((task) => ({
@@ -61,12 +62,17 @@ const EditSponsorPage = ({ showPage, handleClose, sponsor }: EditSponsorPageProp
6162
}
6263
});
6364
const [submitError, setSubmitError] = useState<string | null>(null);
65+
const [logoImage, setLogoImage] = useState<File | null>(null);
66+
6467
if (isLoading) return <LoadingIndicator />;
6568

6669
const onSubmit = async (formData: SponsorPayload) => {
6770
try {
6871
setSubmitError(null);
6972
await mutateAsync({ sponsorId: sponsor.sponsorId, ...formData });
73+
if (logoImage) {
74+
await uploadLogo({ sponsorId: sponsor.sponsorId, logoImage });
75+
}
7076
toast.success('Sponsor updated successfully!');
7177
handleClose();
7278
} catch (err: unknown) {
@@ -84,7 +90,13 @@ const EditSponsorPage = ({ showPage, handleClose, sponsor }: EditSponsorPageProp
8490
title="Edit Sponsor"
8591
component={
8692
<Box display="flex" flexDirection="column" alignItems="flex-end">
87-
<SponsorForm control={control} errors={errors} setValue={setValue} defaultValues={sponsor}></SponsorForm>
93+
<SponsorForm
94+
control={control}
95+
errors={errors}
96+
setValue={setValue}
97+
defaultValues={sponsor}
98+
onLogoImageChange={setLogoImage}
99+
/>
88100
{submitError && (
89101
<Box color="error.main" mb={2} fontWeight="bold">
90102
{submitError}

0 commit comments

Comments
 (0)