Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions backend/src/grant/grant.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,7 @@ async makeGrantsInactive(grantId: number): Promise<Grant> {
userId,
message,
alertTime: alertTime as TDateISO,
sent: false,
};
await this.notificationService.createNotification(notification);
}
Expand All @@ -272,6 +273,7 @@ async makeGrantsInactive(grantId: number): Promise<Grant> {
userId,
message,
alertTime: alertTime as TDateISO,
sent: false,
};
await this.notificationService.createNotification(notification);
}
Expand Down
22 changes: 15 additions & 7 deletions backend/src/notifications/__test__/notification.service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,28 +80,32 @@ describe('NotificationController', () => {
notificationId: '1',
userId: 'user-1',
message: 'New Grant Created 🎉 ',
alertTime: '2024-01-15T10:30:00.000Z'
alertTime: '2024-01-15T10:30:00.000Z',
sent: false
} as Notification;

mockNotification_id1_user2 = {
notificationId: '1',
userId: 'user-2',
message: 'New Grant Created',
alertTime: '2025-01-15T10:30:00.000Z'
alertTime: '2025-01-15T10:30:00.000Z',
sent: false
} as Notification;

mockNotification_id2_user1= {
notificationId: '2',
userId: 'user-1',
message: 'New Grant Created',
alertTime: '2025-01-15T10:30:00.000Z'
alertTime: '2025-01-15T10:30:00.000Z',
sent: false
} as Notification;

mockNotification_id2_user2= {
notificationId: '2',
userId: 'user-2',
message: 'New Grant Created',
alertTime: '2025-01-15T10:30:00.000Z'
alertTime: '2025-01-15T10:30:00.000Z',
sent: false
} as Notification;

mockPut.mockReturnValue({ promise: mockPromise });
Expand Down Expand Up @@ -292,6 +296,7 @@ describe('NotificationController', () => {
userId : 'user-456',
message : 'Test notification',
alertTime : '2024-01-15T10:30:00.000Z',
sent: false
} as Notification;
const result = await notificationService.createNotification(mockNotification);
expect(mockPut).toHaveBeenCalledWith({
Expand All @@ -300,7 +305,8 @@ describe('NotificationController', () => {
notificationId: '123',
userId : 'user-456',
message : 'Test notification',
alertTime : '2024-01-15T10:30:00.000Z'
alertTime : '2024-01-15T10:30:00.000Z',
sent: false
},
});
expect(result).toEqual(mockNotification);
Expand All @@ -315,7 +321,8 @@ describe('NotificationController', () => {
notificationId: '123',
userId: 'user-456',
message: 'Test notification',
alertTime: '2024-01-15T10:30:00.000Z'
alertTime: '2024-01-15T10:30:00.000Z',
sent: false
} as Notification;

// Act
Expand All @@ -329,7 +336,8 @@ describe('NotificationController', () => {
notificationId: '123',
userId: 'user-456',
message: 'Test notification',
alertTime: '2024-01-15T10:30:00.000Z'
alertTime: '2024-01-15T10:30:00.000Z',
sent: false
},
});
});
Expand Down
1 change: 1 addition & 0 deletions backend/src/notifications/notification.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export class NotificationService {
Item: {
...notification,
alertTime: alertTime.toISOString(),
sent: false // initialize sent to false when creating a new notification
},
};
await this.dynamoDb.put(params).promise();
Expand Down
27 changes: 25 additions & 2 deletions backend/src/user/user.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,29 @@ export class UserService {
.promise();

this.logger.log(`✓ User ${username} added to Cognito group ${groupName}`);

// Send verification email if moving from Inactive to employee group
if (
previousGroup === UserStatus.Inactive &&
groupName === UserStatus.Employee
) {
try {
await this.sendVerificationEmail(user.email);
this.logger.log(
`✓ Verification email sent to ${user.email} upon group change to ${groupName}`
);
} catch (emailError) {
this.logger.error(
`Failed to send verification email to ${username}:`,
emailError
);
}
}
else {
this.logger.log(
`No verification email sent to ${username}. Previous group: ${previousGroup}, New group: ${groupName}`
);
}
} catch (cognitoError: any) {
this.logger.error(
`Failed to add ${username} to Cognito group ${groupName}:`,
Expand Down Expand Up @@ -507,9 +530,9 @@ export class UserService {
// sends email to user once account is approved, used in method above when a user
// is added to the Employee or Admin group from Inactive
async sendVerificationEmail(userEmail: string): Promise<AWS.SES.SendEmailResponse> {
// may want to have the default be the BCAN email or something else
// remove actual email and add to env later!!
const fromEmail = process.env.NOTIFICATION_EMAIL_SENDER ||
'u&@nveR1ified-failure@dont-send.com';
'c4cneu.bcan@gmail.com';

const params: AWS.SES.SendEmailRequest = {
Source: fromEmail,
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/Login.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ const Login = observer(() => {
<div className="grid grid-cols-1 gap-x-6 gap-y-4">
<div className="">
<label htmlFor="username" className="block">
Email address
Username
</label>
<div className="flex items-center rounded-md pt-2">
<input
Expand Down
60 changes: 36 additions & 24 deletions frontend/src/animations/AnimatedRoutes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import MainPage from "../main-page/MainPage";
import Login from "../Login";
import Register from "../Register";
import RegisterLanding from "../RegisterLanding";
import { getAppStore } from "../external/bcanSatchel/store"
import RestrictedPage from "../main-page/restricted/RestrictedPage";

/**
* AnimatedRoutes:
Expand All @@ -19,33 +21,43 @@ import RegisterLanding from "../RegisterLanding";
const AnimatedRoutes = observer(() => {
const location = useLocation();
const { isAuthenticated } = useAuthContext();
const user = getAppStore().user;

return (
<Routes location={location}>
<Route
path="/login"
element={isAuthenticated ? <Navigate to="/main/all-grants" /> : <Login />}
/>
<Route
path="/register"
element={
isAuthenticated ? <Navigate to="/main/all-grants" /> : <Register />
}
/>
<Route
path="/registered"
element={
<RegisterLanding />
}
/>
<Route path="/main/*" element={<MainPage/>} />
<Route
path="*"
element={
<Navigate to={isAuthenticated ? "/main/all-grants" : "/login"} />
}
/>
</Routes>
<Route
path="/login"
element={isAuthenticated ? <Navigate to="/main/all-grants" /> : <Login />}
/>
<Route
path="/register"
element={
isAuthenticated ? <Navigate to="/main/all-grants" /> : <Register />
}
/>
<Route
path="/registered"
element={<RegisterLanding />}
/>
<Route path="/restricted" element={<RestrictedPage/>} />

{/* Check user status and render MainPage or redirect */}
<Route
path="/main/*"
element={
user?.position === "Inactive"
? <Navigate to="/restricted" replace />
: <MainPage />
}
/>

<Route
path="*"
element={
<Navigate to={isAuthenticated ? "/main/all-grants" : "/login"} />
}
/>
</Routes>
);
});

Expand Down
16 changes: 3 additions & 13 deletions frontend/src/main-page/dashboard/Dashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,6 @@ import GanttYearGrantTimeline from "./Charts/GanttYearGrantTimeline";
import DonutMoneyApplied from "./Charts/DonutMoneyApplied";
import { ProcessGrantData } from "../grants/filter-bar/processGrantData";
import KPICards from "./Charts/KPICards";
import { Navigate } from "react-router-dom";
import { UserStatus } from "../../../../middle-layer/types/UserStatus";

const Dashboard = observer(() => {
// reset filters on initial render
Expand All @@ -30,7 +28,7 @@ const Dashboard = observer(() => {
updateStartDateFilter(null);
}, []);

const { yearFilter, allGrants, user } = getAppStore();
const { yearFilter, allGrants } = getAppStore();

const uniqueYears = Array.from(
new Set(
Expand All @@ -45,9 +43,7 @@ const Dashboard = observer(() => {

const { grants } = ProcessGrantData();

return user ? (
user?.position !== UserStatus.Inactive ? (
<div className="dashboard-page px-12 py-4 mb-8 ">
return(<div className="dashboard-page px-12 py-4 mb-8 ">
<div className="flex flex-row justify-end gap-4 mb-6">
<CsvExportButton />
<DateFilter />
Expand Down Expand Up @@ -81,13 +77,7 @@ const Dashboard = observer(() => {
<BarYearGrantStatus recentYear={recentYear} grants={grants} />
</div>
</div>
</div>
) : (
<Navigate to="restricted" replace />
)
) : (
<Navigate to="/login" replace />
);
</div>)
});

export default Dashboard;
73 changes: 26 additions & 47 deletions frontend/src/main-page/grants/new-grant/NewGrantModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
} from "../new-grant/processGrantDataEditSave";
import { fetchGrants } from "../filter-bar/processGrantData";
import { observer } from "mobx-react-lite";
import UserDropdown from "./UserDropdown";

/** Attachment type from your middle layer */
enum AttachmentType {
Expand Down Expand Up @@ -221,6 +222,9 @@ const NewGrantModal: React.FC<{

/** Basic validations based on your screenshot fields */
const validateInputs = (): boolean => {
// Timeline check


// Organization validation
if (!organization || organization.trim() === "") {
setErrorMessage("Organization Name is required.");
Expand Down Expand Up @@ -320,11 +324,11 @@ const NewGrantModal: React.FC<{
return false;
}
// Estimated completion time validation
if (estimatedCompletionTimeInHours < 0) {
if (estimatedCompletionTimeInHours <= 0) {
setErrorMessage("Estimated Completion Time cannot be negative.");
return false;
}
if (estimatedCompletionTimeInHours === 0) {
if (estimatedCompletionTimeInHours <= 0) {
setErrorMessage("Estimated Completion Time must be greater than 0.");
return false;
}
Expand Down Expand Up @@ -713,55 +717,30 @@ const NewGrantModal: React.FC<{
<div className="flex w-full mb-16">
{/*BCAN POC div*/}
<div className="w-full pr-3">
<label
className="font-family-helvetica mb-1 flex block tracking-wide text-black sm:text-sm lg:text-base"
htmlFor="grid-zip"
>
BCAN POC *
</label>
{/*Box div*/}
<div
className="items-center flex p-3 rounded h-full"
style={{
backgroundColor: "#F58D5C",
borderColor: "black",
borderWidth: "1px",
borderRadius: "1.2rem",
}}
>
<MdOutlinePerson2 className="w-1/4 h-full sm:p-1 lg:p-2" />
<div className="w-3/4">
<input
style={{
height: "42px",
backgroundColor: "#F2EBE4",
borderStyle: "solid",
borderColor: "black",
borderWidth: "1px",
<label className="font-family-helvetica mb-1 flex block tracking-wide text-black text-lg" htmlFor="grid-zip">
BCAN POC *
</label>
{/*Box div*/}
<div className="items-center flex p-3 rounded h-full" style={{backgroundColor: "#F58D5C", borderColor: 'black', borderWidth: '1px', borderRadius:"1.2rem"}}>
<MdOutlinePerson2 className="w-1/4 h-full p-1"/>
<div className="w-3/4">
<UserDropdown
selectedUser={bcanPocName && bcanPocEmail ? {name: bcanPocName, email: bcanPocEmail } : null}
onSelect={(user) => {
setBcanPocName(user.name);
setBcanPocEmail(user.email)
}}
className="font-family-helvetica w-full text-gray-700 rounded"
id="grid-city"
placeholder="Name"
value={bcanPocName}
onChange={(e) => setBcanPocName(e.target.value)}
/>
<input
style={{
height: "42px",
backgroundColor: "#F2EBE4",
borderStyle: "solid",
borderColor: "black",
borderWidth: "1px",
}}
className="font-family-helvetica w-full text-gray-700 rounded"
id="grid-city"
placeholder="e-mail"
value={bcanPocEmail}
onChange={(e) => setBcanPocEmail(e.target.value)}
/>
/>
<input style={{height: "48px",backgroundColor: '#F2EBE4', borderStyle: 'solid', borderColor: 'black', borderWidth: '1px'}}
className="font-family-helvetica w-full text-gray-700 rounded"
placeholder="e-mail"
value={bcanPocEmail}
readOnly
/>
</div>
</div>
</div>
</div>

{/*Grant Provider POC div*/}
<div className="w-full pl-3">
Expand Down
Loading