diff --git a/backend/src/grant/grant.controller.ts b/backend/src/grant/grant.controller.ts index c1b14b5..fe80973 100644 --- a/backend/src/grant/grant.controller.ts +++ b/backend/src/grant/grant.controller.ts @@ -1,4 +1,4 @@ -import { Controller, Get, Param, Put, Body } from '@nestjs/common'; +import { Controller, Get, Param, Put, Body, Patch } from '@nestjs/common'; import { GrantService } from './grant.service'; @Controller('grant') @@ -29,4 +29,14 @@ export class GrantController { return await this.grantService.unarchiveGrants(grantIds) } + @Put('save/status') + async saveStatus( + @Body('status') status: string + ) { + await this.grantService.updateGrant(1, 'status', status) + return { message: 'Status has been updated' }; + } + + + } \ No newline at end of file diff --git a/backend/src/grant/grant.model.ts b/backend/src/grant/grant.model.ts index 307ff13..35859f8 100644 --- a/backend/src/grant/grant.model.ts +++ b/backend/src/grant/grant.model.ts @@ -14,4 +14,6 @@ export interface Grant { attached_resources: string[]; comments: string[]; isArchived : boolean; -} \ No newline at end of file +} + +// TODO: [JAN-13} Switch deadline to Proper "Date Time" diff --git a/backend/src/grant/grant.service.ts b/backend/src/grant/grant.service.ts index 99018f6..f51fbc0 100644 --- a/backend/src/grant/grant.service.ts +++ b/backend/src/grant/grant.service.ts @@ -9,6 +9,7 @@ export class GrantService { // function to retrieve all grants in our database async getAllGrants(): Promise { + // loads in the environment variable for the table now const params = { TableName: process.env.DYNAMODB_GRANT_TABLE_NAME || 'TABLE_FAILURE', }; @@ -79,4 +80,38 @@ export class GrantService { }; return successfulUpdates; } + + /** + * Given primary key, attribute name, and the content to update, queries database to update + * that info. Returns true if operation was successful. Assumes inputs are valid. + * @param grantId + * @param attributeName + * @param newValue + */ + async updateGrant(grantId: number, attributeName: string, newValue: string): Promise { + const params = { + TableName: process.env.DYNAMODB_GRANT_TABLE_NAME || 'TABLE_FAILURE', + Key: { + grantId: grantId + }, + UpdateExpression: `set #s = :newValue`, + ExpressionAttributeNames: { + '#s': attributeName, + }, + ExpressionAttributeValues: { + ":newValue": newValue, + }, + ReturnValues: "UPDATED_NEW", + } + console.log(params); + + try { + const result = await this.dynamoDb.update(params).promise(); + console.log(result); + } catch(err) { + console.log(err); + throw new Error(`Failed to update Grant ${grantId} attribute + ${attributeName} with ${newValue}`); + } + } } \ No newline at end of file diff --git a/backend/src/main.ts b/backend/src/main.ts index b8ddad5..4a5ce3d 100644 --- a/backend/src/main.ts +++ b/backend/src/main.ts @@ -9,10 +9,13 @@ async function bootstrap() { region: process.env.AWS_REGION, accessKeyId: process.env.OPEN_HATCH, secretAccessKey: process.env.CLOSED_HATCH - }); + }); + const app = await NestFactory.create(AppModule); app.enableCors(); await app.listen(3001); } dotenv.config(); bootstrap(); + + diff --git a/backend/src/notifications/notification.module.ts b/backend/src/notifications/notification.module.ts index c01890e..6a4ff79 100644 --- a/backend/src/notifications/notification.module.ts +++ b/backend/src/notifications/notification.module.ts @@ -2,10 +2,11 @@ import { Module } from '@nestjs/common'; import { NotificationController } from './notification.controller'; import { NotificationService } from './notifcation.service'; - @Module({ - providers: [NotificationService], - controllers: [NotificationController], - exports: [NotificationService], + providers: [NotificationService], // providers perform business logic + controllers: [NotificationController], // controllers directly take in http requests + // and are the starting point for anything happening + exports: [NotificationService], // by putting it under exports, this service is available + // to other modules outside of this }) export class NotificationsModule {} \ No newline at end of file diff --git a/frontend/package.json b/frontend/package.json index b1fd242..5ed066f 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -47,4 +47,4 @@ "vite-tsconfig-paths": "^5.1.4", "vitest": "^2.1.8" } -} +} \ No newline at end of file diff --git a/frontend/src/App.css b/frontend/src/App.css index 6cfcbf0..9e6597b 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -1,45 +1,9 @@ #root { - max-width: 1280px; + max-width: 100%; margin: 0 auto; - padding: 2rem; text-align: center; } -.logo { - height: 6em; - padding: 1.5em; - will-change: filter; - transition: filter 300ms; -} -.logo:hover { - filter: drop-shadow(0 0 2em #646cffaa); -} -.logo.react:hover { - filter: drop-shadow(0 0 2em #61dafbaa); -} - -@keyframes logo-spin { - from { - transform: rotate(0deg); - } - to { - transform: rotate(360deg); - } -} - -@media (prefers-reduced-motion: no-preference) { - a:nth-of-type(2) .logo { - animation: logo-spin infinite 20s linear; - } -} - -.card { - padding: 2em; -} - -.read-the-docs { - color: #888; -} .app-container { display: flex; diff --git a/frontend/src/external/bcanSatchel/actions.ts b/frontend/src/external/bcanSatchel/actions.ts index b8cd2ae..760016e 100644 --- a/frontend/src/external/bcanSatchel/actions.ts +++ b/frontend/src/external/bcanSatchel/actions.ts @@ -1,4 +1,5 @@ import { action } from 'satcheljs'; +import { Grant } from './store.js' /** * Set whether the user is authenticated, update the user object, @@ -24,3 +25,11 @@ export const updateUserProfile = action('updateUserProfile', (user: any) => ({ * Completely log out the user (clear tokens, user data, etc.). */ export const logoutUser = action('logoutUser'); + +/** + * Moves along the all grants that are fetched from back end to mutator. + */ +export const fetchAllGrants = action( + 'fetchAllGrants', + (grants: Grant[]) => ({grants}) +); diff --git a/frontend/src/external/bcanSatchel/mutators.ts b/frontend/src/external/bcanSatchel/mutators.ts index e423df7..f8ea033 100644 --- a/frontend/src/external/bcanSatchel/mutators.ts +++ b/frontend/src/external/bcanSatchel/mutators.ts @@ -1,5 +1,5 @@ import { mutator } from 'satcheljs'; -import { setAuthState, updateUserProfile, logoutUser } from './actions'; +import { setAuthState, updateUserProfile, logoutUser, fetchAllGrants } from './actions'; import { getAppStore } from './store'; /** @@ -34,3 +34,12 @@ mutator(logoutUser, () => { store.user = null; store.accessToken = null; }); + + +/** + * Reassigns all grants to new grants from the backend. + */ +mutator(fetchAllGrants, (actionMessage) => { + const store = getAppStore(); + store.allGrants = actionMessage.grants; +}); diff --git a/frontend/src/external/bcanSatchel/store.ts b/frontend/src/external/bcanSatchel/store.ts index c05bb42..027d6b8 100644 --- a/frontend/src/external/bcanSatchel/store.ts +++ b/frontend/src/external/bcanSatchel/store.ts @@ -10,6 +10,26 @@ export interface AppState { isAuthenticated: boolean; user: User | null; accessToken: string | null; + allGrants: Grant[] | [] +} + +// model for Grant objects, matches exactly the same as backend grant.model +// TODO: should synchronize from same file? +export interface Grant { + grantId: number; + organization_name: string; + description: string; + is_bcan_qualifying: boolean; + status: string; + amount: number; + deadline: string; + notifications_on_for_user: boolean; + reporting_requirements: string; + restrictions: string; + point_of_contacts: string[]; + attached_resources: string[]; + comments: string[]; + isArchived : boolean; } // Define initial state @@ -17,6 +37,7 @@ const initialState: AppState = { isAuthenticated: false, user: null, accessToken: null, + allGrants: [] }; const store = createStore('appStore', initialState); diff --git a/frontend/src/grant-info/components/GrantAttributes.tsx b/frontend/src/grant-info/components/GrantAttributes.tsx index bdcce6f..b915cd0 100644 --- a/frontend/src/grant-info/components/GrantAttributes.tsx +++ b/frontend/src/grant-info/components/GrantAttributes.tsx @@ -1,18 +1,29 @@ -import React from 'react'; +import React, { useContext} from 'react'; import './styles/GrantAttributes.css'; +import {StatusContext} from './StatusContext.tsx'; interface GrantAttributesProps { isEditing: boolean; } export const GrantAttributes: React.FC = ({isEditing}) => { + + // placeholder for now before reworking, will remove redundant useState() + const { curStatus, setCurStatus } = useContext(StatusContext); + + const handleChange = (event: React.ChangeEvent) => { + setCurStatus(event.target.value); + }; + return (
Status
{isEditing ? ( ) : ( diff --git a/frontend/src/grant-info/components/GrantItem.tsx b/frontend/src/grant-info/components/GrantItem.tsx index 8422969..e4c621f 100644 --- a/frontend/src/grant-info/components/GrantItem.tsx +++ b/frontend/src/grant-info/components/GrantItem.tsx @@ -2,30 +2,48 @@ import React, { useState } from "react"; import "./styles/GrantItem.css"; import { GrantAttributes } from "./GrantAttributes"; import GrantDetails from "./GrantDetails"; +import { StatusContext } from "./StatusContext"; +import { Grant } from "@/external/bcanSatchel/store.ts"; -export interface GrantItemProps { - grantName: string; - applicationDate: Date; - generalStatus: string; - amount: number; - restrictionStatus: string; +interface GrantItemProps { + grant: Grant; } -const GrantItem: React.FC = (props) => { - const { - grantName, - applicationDate, - generalStatus, - amount, - restrictionStatus, - } = props; +// TODO: [JAN-14] Make uneditable field editable (ex: Description, Application Reqs, Additional Notes) +const GrantItem: React.FC = ({ grant }) => { const [isExpanded, setIsExpanded] = useState(false); const [isEditing, setIsEditing] = useState(false); + const [curStatus, setCurStatus] = useState(grant.status); const toggleExpand = () => { setIsExpanded(!isExpanded); }; - const toggleEdit = () => setIsEditing((prev) => !prev); + // when toggleEdit gets saved, then updates the backend to update itself with whatever + // is shown in the front-end + + const toggleEdit = async () => { + if (isEditing) { + // if you are saving + try { + const response = await fetch( + "http://localhost:3001/grant/save/status", + { + method: "PUT", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ status: curStatus }), + } + ); + const result = await response.json(); + console.log(result); + } catch (err) { + console.error("Error saving data:", err); + } + // holding result for now + } + setIsEditing(!isEditing); + }; return ( // class name with either be grant-item or grant-item-expanded @@ -34,21 +52,22 @@ const GrantItem: React.FC = (props) => { className={`grant-summary ${isExpanded ? "expanded" : ""}`} onClick={toggleExpand} > -
  • {grantName}
  • -
  • - {applicationDate.toISOString().split("T")[0]} -
  • -
  • {generalStatus}
  • -
  • ${amount}
  • -
  • {restrictionStatus}
  • +
  • {grant.organization_name}
  • +
  • {"no attribute for app-date"}
  • + //.toISOString().split("T")[0] +
  • {grant.status}
  • +
  • ${grant.amount}
  • +
  • {grant.restrictions}
  • {isExpanded && (

    Community Development Initiative Grant

    - - + + + +