Skip to content

Commit 5c0b192

Browse files
committed
sponsor tasks fix
1 parent 343103c commit 5c0b192

12 files changed

Lines changed: 130 additions & 36 deletions

File tree

src/backend/src/controllers/prospective-sponsor.controllers.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@ export default class ProspectiveSponsorController {
1414
contactEmail,
1515
contactPhone,
1616
contactPosition,
17-
notes
17+
notes,
18+
tasks
1819
} = req.body;
1920

2021
const prospectiveSponsor = await ProspectiveSponsorServices.createProspectiveSponsor(
@@ -29,7 +30,8 @@ export default class ProspectiveSponsorController {
2930
contactEmail,
3031
contactPhone,
3132
contactPosition,
32-
notes
33+
notes,
34+
tasks
3335
);
3436
res.status(200).json(prospectiveSponsor);
3537
} catch (error: unknown) {
@@ -60,7 +62,8 @@ export default class ProspectiveSponsorController {
6062
contactEmail,
6163
contactPhone,
6264
contactPosition,
63-
notes
65+
notes,
66+
tasks
6467
} = req.body;
6568

6669
const updatedProspectiveSponsor = await ProspectiveSponsorServices.editProspectiveSponsor(
@@ -77,7 +80,8 @@ export default class ProspectiveSponsorController {
7780
contactEmail,
7881
contactPhone,
7982
contactPosition,
80-
notes
83+
notes,
84+
tasks
8185
);
8286
res.status(200).json(updatedProspectiveSponsor);
8387
} catch (error: unknown) {

src/backend/src/prisma/migrations/20260205202908_prospective_sponsors/migration.sql

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ CREATE TYPE "Prospective_Sponsor_Status" AS ENUM ('IN_PROGRESS', 'DECLINED', 'NO
1010
-- CreateEnum
1111
CREATE TYPE "First_Contact_Method" AS ENUM ('INBOUND_FORM', 'INBOUND_EMAIL', 'OUTBOUND_EMAIL', 'OTHER');
1212

13+
-- CreateEnum
14+
CREATE TYPE "Sponsor_Value_Type" AS ENUM ('MONETARY', 'STOCK', 'DISCOUNT');
15+
1316
-- DropForeignKey
1417
ALTER TABLE "Sponsor_Task" DROP CONSTRAINT "Sponsor_Task_sponsorId_fkey";
1518

@@ -54,12 +57,21 @@ ALTER TABLE "Sponsor" DROP COLUMN "vendorContact";
5457
-- AddForeignKey: Sponsor -> Sponsor_Contact
5558
ALTER TABLE "Sponsor" ADD CONSTRAINT "Sponsor_contactId_fkey" FOREIGN KEY ("contactId") REFERENCES "Sponsor_Contact"("sponsorContactId") ON DELETE RESTRICT ON UPDATE CASCADE;
5659

60+
-- AlterTable: Sponsor - add value types, stock/discount descriptions, make sponsorValue and sponsorTierId nullable
61+
ALTER TABLE "Sponsor"
62+
ADD COLUMN "valueTypes" "Sponsor_Value_Type"[] DEFAULT ARRAY['MONETARY']::"Sponsor_Value_Type"[],
63+
ADD COLUMN "stockDescription" TEXT,
64+
ADD COLUMN "discountDescription" TEXT;
65+
66+
ALTER TABLE "Sponsor" ALTER COLUMN "sponsorValue" DROP NOT NULL;
67+
ALTER TABLE "Sponsor" ALTER COLUMN "sponsorTierId" DROP NOT NULL;
68+
5769
-- AlterTable: Sponsor_Task
5870
ALTER TABLE "Sponsor_Task" ADD COLUMN "done" BOOLEAN NOT NULL DEFAULT false,
5971
ADD COLUMN "prospectiveSponsorId" TEXT,
6072
ALTER COLUMN "sponsorId" DROP NOT NULL;
6173

62-
-- CreateTable: Prospective_Sponsor (with contactId FK instead of inline fields)
74+
-- CreateTable: Prospective_Sponsor
6375
CREATE TABLE "Prospective_Sponsor" (
6476
"prospectiveSponsorId" TEXT NOT NULL,
6577
"organizationId" TEXT NOT NULL,
@@ -71,6 +83,7 @@ CREATE TABLE "Prospective_Sponsor" (
7183
"firstContactMethod" "First_Contact_Method" NOT NULL,
7284
"contactorUserId" TEXT NOT NULL,
7385
"contactId" TEXT NOT NULL,
86+
"notes" TEXT,
7487
"dateDeleted" TIMESTAMP(3),
7588

7689
CONSTRAINT "Prospective_Sponsor_pkey" PRIMARY KEY ("prospectiveSponsorId")

src/backend/src/prisma/migrations/20260214000000_sponsor_value_types/migration.sql

Lines changed: 0 additions & 14 deletions
This file was deleted.

src/backend/src/prisma/migrations/20260215000000_optional_sponsor_tier/migration.sql

Lines changed: 0 additions & 2 deletions
This file was deleted.

src/backend/src/routes/prospective-sponsor.routes.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,12 @@ prospectiveSponsorRouter.post(
2424
nonEmptyString(body('contactPhone')).optional({ checkFalsy: true }),
2525
nonEmptyString(body('contactPosition')).optional({ checkFalsy: true }),
2626
nonEmptyString(body('notes')).optional({ checkFalsy: true }),
27+
body('tasks').optional().isArray(),
28+
isDate(body('tasks.*.dueDate')).optional(),
29+
isDate(body('tasks.*.notifyDate')).optional({ checkFalsy: true }),
30+
nonEmptyString(body('tasks.*.assigneeUserId')).optional({ checkFalsy: true }),
31+
nonEmptyString(body('tasks.*.notes')).optional(),
32+
body('tasks.*.done').optional().isBoolean(),
2733
validateInputs,
2834
ProspectiveSponsorController.createProspectiveSponsor
2935
);
@@ -45,6 +51,13 @@ prospectiveSponsorRouter.post(
4551
nonEmptyString(body('contactPhone')).optional({ checkFalsy: true }),
4652
nonEmptyString(body('contactPosition')).optional({ checkFalsy: true }),
4753
nonEmptyString(body('notes')).optional({ checkFalsy: true }),
54+
body('tasks').optional().isArray(),
55+
nonEmptyString(body('tasks.*.sponsorTaskId')).optional({ checkFalsy: true }),
56+
isDate(body('tasks.*.dueDate')).optional(),
57+
isDate(body('tasks.*.notifyDate')).optional({ checkFalsy: true }),
58+
nonEmptyString(body('tasks.*.assigneeUserId')).optional({ checkFalsy: true }),
59+
nonEmptyString(body('tasks.*.notes')).optional(),
60+
body('tasks.*.done').optional().isBoolean(),
4861
validateInputs,
4962
ProspectiveSponsorController.editProspectiveSponsor
5063
);

src/backend/src/services/prospective-sponsor.services.ts

Lines changed: 74 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import {
2+
CreateSponsorTask,
23
FirstContactMethod,
34
isHead,
45
ProspectiveSponsor,
@@ -39,7 +40,8 @@ export default class ProspectiveSponsorServices {
3940
contactEmail?: string,
4041
contactPhone?: string,
4142
contactPosition?: string,
42-
notes?: string
43+
notes?: string,
44+
tasks?: CreateSponsorTask[]
4345
): Promise<ProspectiveSponsor> {
4446
if (!(await isUserFinanceTeamOrHead(submitter, organization.organizationId))) {
4547
throw new AccessDeniedException('Only finance team members or heads can create prospective sponsors');
@@ -82,11 +84,34 @@ export default class ProspectiveSponsorServices {
8284
contactorUserId,
8385
contactId: contact.sponsorContactId,
8486
notes,
85-
organizationId: organization.organizationId
87+
organizationId: organization.organizationId,
88+
tasks: tasks?.length
89+
? {
90+
create: tasks.map((task) => ({
91+
dueDate: task.dueDate,
92+
notifyDate: task.notifyDate,
93+
assigneeUserId: task.assigneeUserId,
94+
notes: task.notes
95+
}))
96+
}
97+
: undefined
8698
},
8799
...getProspectiveSponsorQueryArgs(organization.organizationId)
88100
});
89101

102+
if (tasks?.length) {
103+
tasks.forEach(async (task) => {
104+
if (!task.assigneeUserId) return;
105+
const assignee = await prisma.user.findUnique({
106+
where: { userId: task.assigneeUserId },
107+
include: { userSettings: true }
108+
});
109+
if (assignee) {
110+
await notifySponsorTaskAssignee(assignee, task, organizationName);
111+
}
112+
});
113+
}
114+
90115
return prospectiveSponsorTransformer(prospectiveSponsor);
91116
}
92117

@@ -122,7 +147,8 @@ export default class ProspectiveSponsorServices {
122147
contactEmail?: string,
123148
contactPhone?: string,
124149
contactPosition?: string,
125-
notes?: string
150+
notes?: string,
151+
tasks?: CreateSponsorTask[]
126152
): Promise<ProspectiveSponsor> {
127153
if (!(await isUserFinanceTeamOrHead(submitter, organization.organizationId))) {
128154
throw new AccessDeniedException('Only finance team members or heads can edit prospective sponsors');
@@ -133,7 +159,8 @@ export default class ProspectiveSponsorServices {
133159
}
134160

135161
const oldProspectiveSponsor = await prisma.prospective_Sponsor.findUnique({
136-
where: { prospectiveSponsorId, organizationId: organization.organizationId }
162+
where: { prospectiveSponsorId, organizationId: organization.organizationId },
163+
include: { tasks: true }
137164
});
138165

139166
if (!oldProspectiveSponsor) throw new NotFoundException('ProspectiveSponsor', prospectiveSponsorId);
@@ -161,6 +188,49 @@ export default class ProspectiveSponsorServices {
161188
throw new NotFoundException('User', contactorUserId);
162189
}
163190

191+
// Upsert tasks if provided
192+
if (tasks) {
193+
const incomingTaskIds = new Set(tasks.filter((t) => t.sponsorTaskId).map((t) => t.sponsorTaskId!));
194+
const tasksToDelete = oldProspectiveSponsor.tasks.filter((t) => !incomingTaskIds.has(t.sponsorTaskId));
195+
const tasksToUpdate = tasks.filter((t) => t.sponsorTaskId);
196+
const tasksToCreate = tasks.filter((t) => !t.sponsorTaskId);
197+
198+
if (tasksToDelete.length > 0) {
199+
await prisma.sponsor_Task.deleteMany({
200+
where: { sponsorTaskId: { in: tasksToDelete.map((t) => t.sponsorTaskId) } }
201+
});
202+
}
203+
204+
await Promise.all(
205+
tasksToUpdate.map((t) =>
206+
prisma.sponsor_Task.update({
207+
where: { sponsorTaskId: t.sponsorTaskId! },
208+
data: {
209+
dueDate: t.dueDate,
210+
notifyDate: t.notifyDate,
211+
assigneeUserId: t.assigneeUserId || null,
212+
notes: t.notes,
213+
done: t.done ?? false
214+
}
215+
})
216+
)
217+
);
218+
219+
await Promise.all(
220+
tasksToCreate.map((t) =>
221+
this.createProspectiveSponsorTask(
222+
submitter,
223+
organization,
224+
prospectiveSponsorId,
225+
t.dueDate,
226+
t.notes,
227+
t.notifyDate,
228+
t.assigneeUserId
229+
)
230+
)
231+
);
232+
}
233+
164234
await prisma.sponsor_Contact.update({
165235
where: { sponsorContactId: oldProspectiveSponsor.contactId },
166236
data: { name: contactName, email: contactEmail, phone: contactPhone, position: contactPosition }

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -784,7 +784,7 @@ export const toggleSponsorTaskDone = (sponsorTaskId: string) => {
784784

785785
/**************** Prospective Sponsors API ****************/
786786

787-
import { ProspectiveSponsor, FirstContactMethod, ProspectiveSponsorStatus } from 'shared';
787+
import { ProspectiveSponsor, FirstContactMethod, ProspectiveSponsorStatus, CreateSponsorTask } from 'shared';
788788
import { prospectiveSponsorTransformer } from './transformers/prospective-sponsor.transformer';
789789

790790
export interface CreateProspectiveSponsorPayload {
@@ -798,6 +798,7 @@ export interface CreateProspectiveSponsorPayload {
798798
contactPhone?: string;
799799
contactPosition?: string;
800800
notes?: string;
801+
tasks?: CreateSponsorTask[];
801802
}
802803

803804
export interface EditProspectiveSponsorPayload {
@@ -812,6 +813,7 @@ export interface EditProspectiveSponsorPayload {
812813
contactPhone?: string;
813814
contactPosition?: string;
814815
notes?: string;
816+
tasks?: CreateSponsorTask[];
815817
}
816818

817819
export interface AcceptProspectiveSponsorPayload {

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,8 @@ const CreateProspectiveSponsorPage = ({ showPage, handleClose }: CreateProspecti
6767
contactEmail: formData.contactEmail || undefined,
6868
contactPhone: formData.contactPhone || undefined,
6969
contactPosition: formData.contactPosition || undefined,
70-
notes: formData.notes || undefined
70+
notes: formData.notes || undefined,
71+
tasks: formData.tasks
7172
});
7273
toast.success('Prospective sponsor created successfully!');
7374
handleClose();

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,8 @@ const EditProspectiveSponsorPage = ({
8686
contactEmail: formData.contactEmail || undefined,
8787
contactPhone: formData.contactPhone || undefined,
8888
contactPosition: formData.contactPosition || undefined,
89-
notes: formData.notes || undefined
89+
notes: formData.notes || undefined,
90+
tasks: formData.tasks
9091
});
9192
toast.success('Prospective sponsor updated successfully!');
9293
handleClose();

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

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ export interface ProspectiveSponsorFormInputs {
4747
notifyDate?: Date;
4848
assigneeUserId?: string;
4949
notes: string;
50+
done?: boolean;
5051
}[];
5152
}
5253

@@ -107,7 +108,8 @@ export const prospectiveSponsorSchema = yup.object().shape({
107108
dueDate: yup.date().required('Due date is required'),
108109
notifyDate: yup.date().optional(),
109110
assigneeUserId: yup.string().optional(),
110-
notes: yup.string().required('Notes are required')
111+
notes: yup.string().required('Notes are required'),
112+
done: yup.boolean().optional()
111113
})
112114
)
113115
.required()
@@ -379,7 +381,8 @@ export const ProspectiveSponsorForm: React.FC<ProspectiveSponsorFormProps> = ({
379381
dueDate: new Date(),
380382
notifyDate: undefined,
381383
assigneeUserId: undefined,
382-
notes: ''
384+
notes: '',
385+
done: false
383386
})
384387
}
385388
sx={{ mt: 2 }}

0 commit comments

Comments
 (0)