From bb96c484a137e53aa51b63f3ca84a410e3af13df Mon Sep 17 00:00:00 2001 From: Jane Kamata Date: Thu, 4 Dec 2025 18:58:04 -0500 Subject: [PATCH 1/6] Frontend bug fixes --- .../main-page/grants/grant-list/GrantItem.tsx | 1175 ++++++++------- .../grants/new-grant/NewGrantModal.tsx | 1307 +++++++++++------ .../main-page/grants/styles/NewGrantModal.css | 1 - 3 files changed, 1458 insertions(+), 1025 deletions(-) diff --git a/frontend/src/main-page/grants/grant-list/GrantItem.tsx b/frontend/src/main-page/grants/grant-list/GrantItem.tsx index d76eab4..93e246c 100644 --- a/frontend/src/main-page/grants/grant-list/GrantItem.tsx +++ b/frontend/src/main-page/grants/grant-list/GrantItem.tsx @@ -19,620 +19,687 @@ interface GrantItemProps { defaultExpanded?: boolean; } -const GrantItem: React.FC = observer(({ grant, defaultExpanded = false }) => { - const [isExpanded, setIsExpanded] = useState(defaultExpanded); - const [isEditing, setIsEditing] = useState(false); - const [curGrant, setCurGrant] = useState(grant); - const [showNewGrantModal, setShowNewGrantModal] = useState(false); - const [wasGrantSubmitted, setWasGrantSubmitted] = useState(false); - const [showDeleteModal, setShowDeleteModal] = useState(false); - - // Track whether each custom dropdown is open. - const [qualifyDropdownOpen, setQualifyDropdownOpen] = useState(false); - const [statusDropdownOpen, setStatusDropdownOpen] = useState(false); - - const toggleExpand = () => { - // Toggle edit mode off now that we are leaving this specific grant in view - if (isExpanded) { - toggleEdit(); - } - setIsExpanded(!isExpanded); - }; - - // Sync isExpanded with the defaultExpanded prop. - useEffect(() => { - setIsExpanded(defaultExpanded); - }, [defaultExpanded]); - - // If the NewGrantModal has been closed and a new grant submitted (or existing grant edited), - // fetch the grant at this index so that all new changes are immediately reflected - useEffect(() => { - const updateGrant = async () => { - if (!showNewGrantModal && wasGrantSubmitted) { - try { - const response = await api(`/grant/${grant.grantId}`, { - method: "GET", - headers: { - "Content-Type": "application/json", +const GrantItem: React.FC = observer( + ({ grant, defaultExpanded = false }) => { + const [isExpanded, setIsExpanded] = useState(defaultExpanded); + const [isEditing, setIsEditing] = useState(false); + const [curGrant, setCurGrant] = useState(grant); + const [showNewGrantModal, setShowNewGrantModal] = useState(false); + const [wasGrantSubmitted, setWasGrantSubmitted] = useState(false); + const [showDeleteModal, setShowDeleteModal] = useState(false); + + const toggleExpand = () => { + // Toggle edit mode off now that we are leaving this specific grant in view + if (isExpanded) { + toggleEdit(); + } + setIsExpanded(!isExpanded); + }; + + // Sync isExpanded with the defaultExpanded prop. + useEffect(() => { + setIsExpanded(defaultExpanded); + }, [defaultExpanded]); + + // If the NewGrantModal has been closed and a new grant submitted (or existing grant edited), + // fetch the grant at this index so that all new changes are immediately reflected + useEffect(() => { + const updateGrant = async () => { + if (!showNewGrantModal && wasGrantSubmitted) { + try { + const response = await api(`/grant/${grant.grantId}`, { + method: "GET", + headers: { + "Content-Type": "application/json", + }, + }); + + if (response.ok) { + const updatedGrant = await response.json(); + setCurGrant(updatedGrant); + console.log("✅ Grant refreshed:", updatedGrant); + } else { + console.error("❌ Failed to fetch updated grant"); + } + } catch (err) { + console.error("Error fetching updated grant:", err); } - }); + setWasGrantSubmitted(false); + } + }; - if (response.ok) { - const updatedGrant = await response.json(); - setCurGrant(updatedGrant); - console.log("✅ Grant refreshed:", updatedGrant); - } else { - console.error("❌ Failed to fetch updated grant"); + updateGrant(); + }, [showNewGrantModal, wasGrantSubmitted]); + + const toggleEdit = async () => { + if (isEditing) { + // Save changes when exiting edit mode. + try { + const response = await api("/grant/save", { + method: "PUT", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(curGrant), + }); + const result = await response.json(); + console.log(result); + } catch (err) { + console.error("Error saving data:", err); } - } catch (err) { - console.error("Error fetching updated grant:", err); } - setWasGrantSubmitted(false); - } - }; - - updateGrant(); -}, [showNewGrantModal, wasGrantSubmitted]); - - const toggleEdit = async () => { - if (isEditing) { - // Save changes when exiting edit mode. + setIsEditing(!isEditing); + }; + + const deleteGrant = async () => { + setShowDeleteModal(false); + + console.log("=== DELETE GRANT DEBUG ==="); + console.log("Current grant:", curGrant); + console.log("Grant ID:", curGrant.grantId); + console.log("Organization:", curGrant.organization); + console.log("Full URL:", `/grant/${curGrant.grantId}`); + try { - const response = await api("/grant/save", { - method: "PUT", + const response = await api(`/grant/${curGrant.grantId}`, { + method: "DELETE", headers: { "Content-Type": "application/json", }, - body: JSON.stringify(curGrant), }); - const result = await response.json(); - console.log(result); - } catch (err) { - console.error("Error saving data:", err); - } - } - setIsEditing(!isEditing); - setQualifyDropdownOpen(false); - setStatusDropdownOpen(false); - }; - - const deleteGrant = async () => { - setShowDeleteModal(false); - - console.log("=== DELETE GRANT DEBUG ==="); - console.log("Current grant:", curGrant); - console.log("Grant ID:", curGrant.grantId); - console.log("Organization:", curGrant.organization); - console.log("Full URL:", `/grant/${curGrant.grantId}`); - - try { - const response = await api(`/grant/${curGrant.grantId}`, { - method: "DELETE", - headers: { - "Content-Type": "application/json", - } - }); - - console.log("Response status:", response.status); - console.log("Response ok:", response.ok); - - if (response.ok) { - console.log("✅ Grant deleted successfully"); - // Refetch grants to update UI - await fetchGrants(); - } else { - // Get error details - const errorText = await response.text(); - console.error("❌ Error response:", errorText); - - let errorData; - try { - errorData = JSON.parse(errorText); - console.error("Parsed error:", errorData); - } catch { - console.error("Could not parse error response"); + + console.log("Response status:", response.status); + console.log("Response ok:", response.ok); + + if (response.ok) { + console.log("✅ Grant deleted successfully"); + // Refetch grants to update UI + await fetchGrants(); + } else { + // Get error details + const errorText = await response.text(); + console.error("❌ Error response:", errorText); + + let errorData; + try { + errorData = JSON.parse(errorText); + console.error("Parsed error:", errorData); + } catch { + console.error("Could not parse error response"); + } } + } catch (err) { + console.error("=== EXCEPTION CAUGHT ==="); + console.error( + "Error type:", + err instanceof Error ? "Error" : typeof err + ); + console.error( + "Error message:", + err instanceof Error ? err.message : err + ); + console.error("Full error:", err); } - } catch (err) { - console.error("=== EXCEPTION CAUGHT ==="); - console.error("Error type:", err instanceof Error ? "Error" : typeof err); - console.error("Error message:", err instanceof Error ? err.message : err); - console.error("Full error:", err); + }; + + function formatDate(isoString: string): string { + const date = new Date(isoString); + const month = String(date.getMonth() + 1).padStart(2, "0"); + const day = String(date.getDate()).padStart(2, "0"); + const year = date.getFullYear(); + return `${month}/${day}/${year}`; } -}; - - function formatDate(isoString: string): string { - const date = new Date(isoString); - const month = String(date.getMonth() + 1).padStart(2, '0'); - const day = String(date.getDate()).padStart(2, '0'); - const year = date.getFullYear(); - return `${month}/${day}/${year}`; - } - function formatCurrency(amount : number): string { - const formattedCurrency = new Intl.NumberFormat('en-US', {style: 'currency',currency: 'USD', - maximumFractionDigits:0}).format(amount); - return formattedCurrency; - } + function formatCurrency(amount: number): string { + const formattedCurrency = new Intl.NumberFormat("en-US", { + style: "currency", + currency: "USD", + maximumFractionDigits: 0, + }).format(amount); + return formattedCurrency; + } - return ( -
-
-
  • - {isExpanded ? : } - {curGrant.organization} -
  • -
  • - {curGrant.application_deadline - ? new Date(curGrant.application_deadline).toLocaleDateString() - : "No date"} -
  • -
  • - {formatCurrency(curGrant.amount)} -
  • -
  • - {isEditing ? ( -
    e.stopPropagation()}> -
    setQualifyDropdownOpen(!qualifyDropdownOpen)}> - -
    - {qualifyDropdownOpen && ( -
    -
    { - e.stopPropagation(); - setCurGrant({ ...curGrant, does_bcan_qualify: true }); - setQualifyDropdownOpen(false); - }} - > - -
    -
    { - e.stopPropagation(); - setCurGrant({ ...curGrant, does_bcan_qualify: false }); - setQualifyDropdownOpen(false); - }} - > - -
    -
    - )} -
    - ) : ( - curGrant.does_bcan_qualify ? ( - + return ( +
    +
    +
  • + {isExpanded ? : } + {curGrant.organization} +
  • +
  • + {curGrant.application_deadline + ? new Date(curGrant.application_deadline).toLocaleDateString() + : "No date"} +
  • +
  • {formatCurrency(curGrant.amount)}
  • +
  • + {curGrant.does_bcan_qualify ? ( + ) : ( - - ) - )} -
  • -
  • - {isEditing ? ( -
    e.stopPropagation()}> -
    setStatusDropdownOpen(!statusDropdownOpen)}> -
    - -
    -
    - {statusDropdownOpen && ( -
    -
    { - e.stopPropagation(); - setCurGrant({ ...curGrant, status: Status.Active }); - setStatusDropdownOpen(false); - }} - > -
    Active
    -
    -
    { - e.stopPropagation(); - setCurGrant({ ...curGrant, status: Status.Inactive }); - setStatusDropdownOpen(false); - }} - > -
    Inactive
    -
    -
    { - e.stopPropagation(); - setCurGrant({ ...curGrant, status: Status.Potential }); - setStatusDropdownOpen(false); - }} - > -
    Potential
    -
    -
    - )} -
    - ) : ( + + )} +
  • +
  • - )} -
  • -
    + +
    -
    - {isExpanded && ( -
    +
    + {isExpanded && ( +
    +
    + {/*div for the two columns above description*/} +
    + {/*Left column */} +
    + {/*Organization name (only div in the first row) */} +
    + +
    {curGrant.organization}
    +
    - {/*div for the two columns above description*/} -
    + {/*Col of gray labels + col of report deadliens (below org name) */} +
    + {/*Left column of gray labels */} +
    + {/*Application date and grant start date row*/} +
    + {/*Application date*/} +
    + +
    + {formatDate(curGrant.application_deadline)} +
    +
    + {/*Grant Start Date */} +
    + +
    + {curGrant.grant_start_date + ? formatDate(curGrant.grant_start_date) + : "Unknown"} +
    +
    + + {/*End application date and grant start date row */} +
    - {/*Left column */} -
    + {/*Estimated completion time row*/} +
    + +
    + {curGrant.estimated_completion_time + ? curGrant.estimated_completion_time + " hours" + : "No est completion time"} +
    +
    + {/*Timeline and Amount row*/} +
    + {/*Timeline*/} +
    + +
    + {curGrant.timeline + ? curGrant.timeline + " years" + : "No timeline"} +
    +
    + {/*Amount */} +
    + +
    + {formatCurrency(curGrant.amount)} +
    +
    + {/*End timeline and amount row */} +
    - {/*Organization name (only div in the first row) */} -
    - -
    {curGrant.organization}
    -
    + {/*End column of gray labels */} +
    - {/*Col of gray labels + col of report deadliens (below org name) */} -
    - {/*Left column of gray labels */} -
    - - {/*Application date and grant start date row*/} -
    - {/*Application date*/} -
    -
    - {/*Grant Start Date */} -
    -
    + + {/*Right column */} +
    + {/*POC row */} +
    + {/*BCAN POC div*/} +
    + + {/*Box div*/} +
    + +
    +

    + {" "} + {curGrant.bcan_poc?.POC_name ?? "Unknown"}{" "} +

    +

    + {" "} + {curGrant.bcan_poc?.POC_email ?? + "----------"}{" "} +

    +
    +
    +
    + + {/*Grant Provider POC div*/} +
    + -
    + +
    - {curGrant.grant_start_date? formatDate(curGrant.grant_start_date) : "Unknown"} +

    + {" "} + {curGrant.grantmaker_poc?.POC_name ?? "Unknown"} +

    +

    + {" "} + {curGrant.grantmaker_poc?.POC_email ?? + "----------"}{" "} +

    +
    +
    + {/*End POC row */}
    - {/*End application date and grant start date row */} -
    + {/* Colored attributes + scope documents row*/} +
    + {/*Colored attributes col */} +
    + {/*Does BCAN qualify */} +
    + +
    + {curGrant.does_bcan_qualify ? "Yes" : "No"} +
    +
    - {/*Estimated completion time row*/} -
    - -
    - {curGrant.estimated_completion_time? curGrant.estimated_completion_time + " hours" : "No est completion time"} -
    -
    + {/*Status*/} +
    + +
    + {curGrant.status} +
    +
    - - {/*End column of gray labels */} -
    + {/*Restriction*/} +
    + +
    + {curGrant.isRestricted + ? "Restricted" + : "Not Restricted"} +
    +
    + {/*End colored attributes col*/} +
    - {/*Report deadlines div*/} -
    - -
    - {/*Map each available report deadline to a div label - If no deadlines, add "No deadlines" text */} - {curGrant.report_deadlines && curGrant.report_deadlines.length > 0 ? ( - curGrant.report_deadlines.map((deadline: string, index: number) => ( + {/*Scope documents div*/} +
    +
    - {formatDate(deadline)} + {/*Map each available report deadline to a div label + If no deadlines, add "No deadlines" text */} + {curGrant.attachments && + curGrant.attachments.length > 0 ? ( + curGrant.attachments.map( + (attachment: Attachment, index: number) => ( +
    + {attachment.url && ( +
    + {attachment.url} +
    + )} +
    + ) + ) + ) : ( +
    + No documents +
    + )}
    - )) - ) : ( -
    No deadlines
    - )} + {/*End scope docs div*/} +
    - {/*End report deadlines div*/} -
    - - {/* End row of gray labels (application date, grant start date, estimated completion time) to next of report deadline + report deadline */} -
    - {/*Timeline and Amount row*/} -
    - {/*Timeline*/} -
    - -
    - {curGrant.timeline? curGrant.timeline + " years" : "No timeline"} -
    -
    - {/*Amount */} -
    - -
    - {formatCurrency(curGrant.amount)} + {/*End right column */}
    -
    - {/*End timeline and amount row */} -
    - - - - {/*End left column */} -
    - {/*Right column */} -
    - {/*POC row */} -
    - {/*BCAN POC div*/} -
    - - {/*Box div*/} -
    - -
    -

    {curGrant.bcan_poc?.POC_name ?? 'Unknown'}

    -

    {curGrant.bcan_poc?.POC_email ?? '----------'}

    -
    -
    + {/*End two main left right columns */}
    - {/*Grant Provider POC div*/} -
    -
    - -
    - - {/*End expanded div */} -
    - )} -
    + )} +
    -
    +
    {showNewGrantModal && ( - {setShowNewGrantModal(false); setWasGrantSubmitted(true);}} + onClose={async () => { + setShowNewGrantModal(false); + setWasGrantSubmitted(true); + }} isOpen={showNewGrantModal} /> )}
    +
    + ); + } +); - -
    - ); -}); - -export default GrantItem; \ No newline at end of file +export default GrantItem; diff --git a/frontend/src/main-page/grants/new-grant/NewGrantModal.tsx b/frontend/src/main-page/grants/new-grant/NewGrantModal.tsx index 18d34ad..6d43854 100644 --- a/frontend/src/main-page/grants/new-grant/NewGrantModal.tsx +++ b/frontend/src/main-page/grants/new-grant/NewGrantModal.tsx @@ -1,13 +1,16 @@ // frontend/src/grant-info/components/NewGrantModal.tsx import React, { useEffect, useState } from "react"; -import CurrencyInput from 'react-currency-input-field'; +import CurrencyInput from "react-currency-input-field"; import "../styles/NewGrantModal.css"; import { MdOutlinePerson2 } from "react-icons/md"; import { FiUpload } from "react-icons/fi"; import { Grant } from "../../../../../middle-layer/types/Grant"; import { TDateISO } from "../../../../../backend/src/utils/date"; import { Status } from "../../../../../middle-layer/types/Status"; -import { createNewGrant, saveGrantEdits } from "../new-grant/processGrantDataEditSave"; +import { + createNewGrant, + saveGrantEdits, +} from "../new-grant/processGrantDataEditSave"; import { fetchGrants } from "../filter-bar/processGrantData"; import { observer } from "mobx-react-lite"; @@ -25,7 +28,11 @@ interface Attachment { } // const FilterBar: React.FC = observer(() => { -const NewGrantModal: React.FC<{ grantToEdit : Grant | null , onClose: () => void, isOpen : boolean }> = observer(({ grantToEdit, onClose, isOpen }) => { +const NewGrantModal: React.FC<{ + grantToEdit: Grant | null; + onClose: () => void; + isOpen: boolean; +}> = observer(({ grantToEdit, onClose, isOpen }) => { /* grantId: number; organization: string; @@ -47,79 +54,118 @@ const NewGrantModal: React.FC<{ grantToEdit : Grant | null , onClose: () => void // Form fields, renamed to match your screenshot // Used - const [organization, _setOrganization] = useState(grantToEdit? grantToEdit.organization : ""); + const [organization, _setOrganization] = useState( + grantToEdit ? grantToEdit.organization : "" + ); // Helper function to normalize dates to YYYY-MM-DD format const normalizeDateToISO = (date: TDateISO | ""): TDateISO | "" => { if (!date) return ""; // If it has time component, extract just the date part - return date.split('T')[0] as TDateISO; + return date.split("T")[0] as TDateISO; }; // Used const [applicationDate, _setApplicationDate] = useState( - grantToEdit?.application_deadline ? normalizeDateToISO(grantToEdit.application_deadline) : "" -); + grantToEdit?.application_deadline + ? normalizeDateToISO(grantToEdit.application_deadline) + : "" + ); -const [grantStartDate, _setGrantStartDate] = useState( - grantToEdit?.grant_start_date ? normalizeDateToISO(grantToEdit.grant_start_date) : "" -); + const [grantStartDate, _setGrantStartDate] = useState( + grantToEdit?.grant_start_date + ? normalizeDateToISO(grantToEdit.grant_start_date) + : "" + ); + + const [reportDates, setReportDates] = useState<(TDateISO | "")[]>( + grantToEdit?.report_deadlines?.map((date) => normalizeDateToISO(date)) || [] + ); -const [reportDates, setReportDates] = useState<(TDateISO | "")[]>( - grantToEdit?.report_deadlines?.map(date => normalizeDateToISO(date)) || [] -); - // Used - const [timelineInYears, _setTimelineInYears] = useState(grantToEdit? grantToEdit.timeline : 1); + const [timelineInYears, _setTimelineInYears] = useState( + grantToEdit ? grantToEdit.timeline : 1 + ); // Used - const [estimatedCompletionTimeInHours, _setEstimatedCompletionTimeInHours] = useState(grantToEdit? grantToEdit.estimated_completion_time : 10); + const [estimatedCompletionTimeInHours, _setEstimatedCompletionTimeInHours] = + useState(grantToEdit ? grantToEdit.estimated_completion_time : 10); // Used const [doesBcanQualify, _setDoesBcanQualify] = useState( - grantToEdit ? (grantToEdit.does_bcan_qualify ? "yes" : "no") : "" -); + grantToEdit ? (grantToEdit.does_bcan_qualify ? "yes" : "no") : "" + ); // Used - const [isRestricted, _setIsRestricted] = useState(grantToEdit? String(grantToEdit.isRestricted) : ""); + const [isRestricted, _setIsRestricted] = useState( + grantToEdit ? String(grantToEdit.isRestricted) : "" + ); // Used const [status, _setStatus] = useState( - grantToEdit ? grantToEdit.status : "" -); + grantToEdit ? grantToEdit.status : "" + ); // Used - const [amount, _setAmount] = useState(grantToEdit? grantToEdit.amount : 1000); + const [amount, _setAmount] = useState( + grantToEdit ? grantToEdit.amount : 1000 + ); // Used - const [description, _setDescription] = useState(grantToEdit? grantToEdit.description? grantToEdit.description : "" : ""); + const [description, _setDescription] = useState( + grantToEdit ? (grantToEdit.description ? grantToEdit.description : "") : "" + ); // Attachments array // Used - const [attachments, setAttachments] = useState<(Attachment)[]>(grantToEdit?.attachments || []); + const [attachments, setAttachments] = useState( + grantToEdit?.attachments || [] + ); // Used const [isAddingAttachment, setIsAddingAttachment] = useState(false); const [currentAttachment, setCurrentAttachment] = useState({ - attachment_name: "", - url: "", - type: AttachmentType.SCOPE_DOCUMENT, - }); - + attachment_name: "", + url: "", + type: AttachmentType.SCOPE_DOCUMENT, + }); // Used - const [bcanPocName, setBcanPocName] = useState(grantToEdit? grantToEdit.bcan_poc? grantToEdit.bcan_poc.POC_name: '' : ''); + const [bcanPocName, setBcanPocName] = useState( + grantToEdit + ? grantToEdit.bcan_poc + ? grantToEdit.bcan_poc.POC_name + : "" + : "" + ); // Used? - const [bcanPocEmail, setBcanPocEmail] = useState(grantToEdit? grantToEdit.bcan_poc? grantToEdit.bcan_poc.POC_email : '' : ''); + const [bcanPocEmail, setBcanPocEmail] = useState( + grantToEdit + ? grantToEdit.bcan_poc + ? grantToEdit.bcan_poc.POC_email + : "" + : "" + ); // Used - const [grantProviderPocName, setGrantProviderPocName] = useState(grantToEdit? grantToEdit.grantmaker_poc? grantToEdit.grantmaker_poc.POC_name: '' : ''); + const [grantProviderPocName, setGrantProviderPocName] = useState( + grantToEdit + ? grantToEdit.grantmaker_poc + ? grantToEdit.grantmaker_poc.POC_name + : "" + : "" + ); // Used - const [grantProviderPocEmail, setGrantProviderPocEmail] = useState(grantToEdit? grantToEdit.grantmaker_poc? grantToEdit.grantmaker_poc.POC_email: '' : ''); - + const [grantProviderPocEmail, setGrantProviderPocEmail] = useState( + grantToEdit + ? grantToEdit.grantmaker_poc + ? grantToEdit.grantmaker_poc.POC_email + : "" + : "" + ); // For error handling // @ts-ignore const [_errorMessage, setErrorMessage] = useState(""); const [showErrorPopup, setShowErrorPopup] = useState(false); - + // State to track if form was submitted successfully const [wasSubmitted, setWasSubmitted] = useState(false); @@ -164,7 +210,6 @@ const [reportDates, setReportDates] = useState<(TDateISO | "")[]>( }); }; - // Used const _removeAttachment = (index: number) => { const updated = attachments.filter((_, i) => i !== index); @@ -176,168 +221,184 @@ const [reportDates, setReportDates] = useState<(TDateISO | "")[]>( /** Basic validations based on your screenshot fields */ const validateInputs = (): boolean => { - // Organization validation - if (!organization || organization.trim() === "") { - setErrorMessage("Organization Name is required."); - return false; - } - // Does BCAN Qualify validation - if (doesBcanQualify === "") { - setErrorMessage("Set Does BCAN Qualify? to 'yes' or 'no'"); - return false; - } - // Status validation - if (status === "" || status == null) { - setErrorMessage("Status is required."); - return false; - } - const validStatuses = [Status.Active, Status.Inactive, Status.Potential, Status.Pending, Status.Rejected]; - if (!validStatuses.includes(status as Status)) { - setErrorMessage("Invalid status selected."); - return false; - } - // Amount validation - if (amount <= 0) { - setErrorMessage("Amount must be greater than 0."); - return false; - } - if (isNaN(amount) || !isFinite(amount)) { - setErrorMessage("Amount must be a valid number."); - return false; - } - // Date validations - if (!applicationDate || applicationDate.trim() === "") { - setErrorMessage("Application Deadline is required."); - return false; - } - if (!grantStartDate || grantStartDate.trim() === "") { - setErrorMessage("Grant Start Date is required."); - return false; - } - - // const isoDateRegex = /^\d{4}-\d{2}-\d{2}$/; - // if (!isoDateRegex.test(applicationDate)) { - // setErrorMessage("Application Deadline must be in valid date format (YYYY-MM-DD). instead of " + applicationDate); - // return false; - // } - // if (!isoDateRegex.test(grantStartDate)) { - // setErrorMessage("Grant Start Date must be in valid date format (YYYY-MM-DD)."); - // return false; - // } - // Validate dates are actual valid dates - const appDate = new Date(applicationDate); - const startDate = new Date(grantStartDate); - if (isNaN(appDate.getTime())) { - setErrorMessage("Application Deadline is not a valid date."); - return false; - } - if (isNaN(startDate.getTime())) { - setErrorMessage("Grant Start Date is not a valid date."); - return false; - } - // Logical date validation - grant start should typically be after application deadline - if (startDate < appDate) { - setErrorMessage("Grant Start Date should typically be after Application Deadline."); - return false; - } - - // Report deadlines validation - if (reportDates && reportDates.length > 0) { - for (let i = 0; i < reportDates.length; i++) { - const reportDate = reportDates[i]; - - // Skip empty entries (if you allow them) - if (!reportDate) { - setErrorMessage(`Report Date ${i + 1} cannot be empty. Remove it if not needed.`); - return false; - } - - const repDate = new Date(reportDate); - if (isNaN(repDate.getTime())) { - setErrorMessage(`Report Date ${i + 1} is not a valid date.`); - return false; - } - + // Organization validation + if (!organization || organization.trim() === "") { + setErrorMessage("Organization Name is required."); + return false; + } + // Does BCAN Qualify validation + if (doesBcanQualify === "") { + setErrorMessage("Set Does BCAN Qualify? to 'yes' or 'no'"); + return false; + } + // Status validation + if (status === "" || status == null) { + setErrorMessage("Status is required."); + return false; + } + const validStatuses = [ + Status.Active, + Status.Inactive, + Status.Potential, + Status.Pending, + Status.Rejected, + ]; + if (!validStatuses.includes(status as Status)) { + setErrorMessage("Invalid status selected."); + return false; + } + // Amount validation + if (amount <= 0) { + setErrorMessage("Amount must be greater than 0."); + return false; + } + if (isNaN(amount) || !isFinite(amount)) { + setErrorMessage("Amount must be a valid number."); + return false; + } + // Date validations + if (!applicationDate || applicationDate.trim() === "") { + setErrorMessage("Application Deadline is required."); + return false; + } + if (!grantStartDate || grantStartDate.trim() === "") { + setErrorMessage("Grant Start Date is required."); + return false; + } + // const isoDateRegex = /^\d{4}-\d{2}-\d{2}$/; + // if (!isoDateRegex.test(applicationDate)) { + // setErrorMessage("Application Deadline must be in valid date format (YYYY-MM-DD). instead of " + applicationDate); + // return false; + // } + // if (!isoDateRegex.test(grantStartDate)) { + // setErrorMessage("Grant Start Date must be in valid date format (YYYY-MM-DD)."); + // return false; + // } + // Validate dates are actual valid dates + const appDate = new Date(applicationDate); + const startDate = new Date(grantStartDate); + if (isNaN(appDate.getTime())) { + setErrorMessage("Application Deadline is not a valid date."); + return false; } - } - // Timeline validation - if (timelineInYears < 0) { - setErrorMessage("Timeline cannot be negative."); - return false; - } - // Estimated completion time validation - if (estimatedCompletionTimeInHours < 0) { - setErrorMessage("Estimated Completion Time cannot be negative."); - return false; - } - if (estimatedCompletionTimeInHours === 0) { - setErrorMessage("Estimated Completion Time must be greater than 0."); - return false; - } - // Restriction type validation - if (isRestricted === "") { - setErrorMessage("Set Restriction Type to 'restricted' or 'unrestricted'"); - return false; - } - // BCAN POC validation - if (!bcanPocName || bcanPocName.trim() === "") { - setErrorMessage("BCAN Point of Contact Name is required."); - return false; - } - if (!bcanPocEmail || bcanPocEmail.trim() === "") { - setErrorMessage("BCAN Point of Contact Email is required."); - return false; - } - // Email format validation - const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; - if (!emailRegex.test(bcanPocEmail)) { - setErrorMessage("BCAN Point of Contact Email must be a valid email address."); - return false; - } - // Grant Provider POC validation (optional, but if provided must be valid) - if (grantProviderPocName && grantProviderPocName.trim() !== "") { - if (grantProviderPocName.trim().length < 2) { - setErrorMessage("Grant Provider Point of Contact Name must be at least 2 characters."); + if (isNaN(startDate.getTime())) { + setErrorMessage("Grant Start Date is not a valid date."); return false; } - } - if (grantProviderPocEmail && grantProviderPocEmail.trim() !== "") { - if (!emailRegex.test(grantProviderPocEmail)) { - setErrorMessage("Grant Provider Point of Contact Email must be a valid email address."); + // Logical date validation - grant start should typically be after application deadline + if (startDate < appDate) { + setErrorMessage( + "Grant Start Date should typically be after Application Deadline." + ); return false; } - } - // Attachments validation - if (attachments && attachments.length > 0) { - for (let i = 0; i < attachments.length; i++) { - const attachment = attachments[i]; - if (!attachment.attachment_name || attachment.attachment_name.trim() === "") { - setErrorMessage(`Attachment ${i + 1} must have a name.`); - return false; - } - if (!attachment.url || attachment.url.trim() === "") { - setErrorMessage(`Attachment ${i + 1} must have a URL.`); + + // Report deadlines validation + if (reportDates && reportDates.length > 0) { + for (let i = 0; i < reportDates.length; i++) { + const reportDate = reportDates[i]; + + // Skip empty entries (if you allow them) + if (!reportDate) { + setErrorMessage( + `Report Date ${i + 1} cannot be empty. Remove it if not needed.` + ); + return false; + } + + const repDate = new Date(reportDate); + if (isNaN(repDate.getTime())) { + setErrorMessage(`Report Date ${i + 1} is not a valid date.`); + return false; + } + } + } + // Timeline validation + if (timelineInYears < 0) { + setErrorMessage("Timeline cannot be negative."); + return false; + } + // Estimated completion time validation + if (estimatedCompletionTimeInHours < 0) { + setErrorMessage("Estimated Completion Time cannot be negative."); + return false; + } + if (estimatedCompletionTimeInHours === 0) { + setErrorMessage("Estimated Completion Time must be greater than 0."); + return false; + } + // Restriction type validation + if (isRestricted === "") { + setErrorMessage("Set Restriction Type to 'restricted' or 'unrestricted'"); + return false; + } + // BCAN POC validation + if (!bcanPocName || bcanPocName.trim() === "") { + setErrorMessage("BCAN Point of Contact Name is required."); + return false; + } + if (!bcanPocEmail || bcanPocEmail.trim() === "") { + setErrorMessage("BCAN Point of Contact Email is required."); + return false; + } + // Email format validation + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + if (!emailRegex.test(bcanPocEmail)) { + setErrorMessage( + "BCAN Point of Contact Email must be a valid email address." + ); + return false; + } + // Grant Provider POC validation (optional, but if provided must be valid) + if (grantProviderPocName && grantProviderPocName.trim() !== "") { + if (grantProviderPocName.trim().length < 2) { + setErrorMessage( + "Grant Provider Point of Contact Name must be at least 2 characters." + ); return false; - } - // Basic URL validation - try { - new URL(attachment.url); - } catch { - setErrorMessage(`Attachment ${i + 1} URL is not valid.`); + } + } + if (grantProviderPocEmail && grantProviderPocEmail.trim() !== "") { + if (!emailRegex.test(grantProviderPocEmail)) { + setErrorMessage( + "Grant Provider Point of Contact Email must be a valid email address." + ); return false; } } - } - // Description validation (optional but reasonable length if provided) - if (description && description.length > 5000) { - setErrorMessage("Description is too long (max 5000 characters)."); - return false; - } - - return true; -}; + // Attachments validation + if (attachments && attachments.length > 0) { + for (let i = 0; i < attachments.length; i++) { + const attachment = attachments[i]; + if ( + !attachment.attachment_name || + attachment.attachment_name.trim() === "" + ) { + setErrorMessage(`Attachment ${i + 1} must have a name.`); + return false; + } + if (!attachment.url || attachment.url.trim() === "") { + setErrorMessage(`Attachment ${i + 1} must have a URL.`); + return false; + } + // Basic URL validation + try { + new URL(attachment.url); + } catch { + setErrorMessage(`Attachment ${i + 1} URL is not valid.`); + return false; + } + } + } + // Description validation (optional but reasonable length if provided) + if (description && description.length > 5000) { + setErrorMessage("Description is too long (max 5000 characters)."); + return false; + } + return true; + }; const handleSubmit = async () => { if (!validateInputs()) { @@ -347,23 +408,26 @@ const [reportDates, setReportDates] = useState<(TDateISO | "")[]>( const grantData: Grant = { grantId: grantToEdit ? grantToEdit.grantId : 0, - organization : organization, - does_bcan_qualify: (doesBcanQualify === "yes"), + organization: organization, + does_bcan_qualify: doesBcanQualify === "yes", amount, grant_start_date: grantStartDate as TDateISO, application_deadline: applicationDate as TDateISO, - status: status as Status, - bcan_poc: { POC_name: bcanPocName, POC_email: bcanPocEmail }, - grantmaker_poc: (grantProviderPocName && grantProviderPocEmail) ? { POC_name: grantProviderPocName, POC_email: grantProviderPocEmail } : { POC_name: '', POC_email: '' }, + status: status as Status, + bcan_poc: { POC_name: bcanPocName, POC_email: bcanPocEmail }, + grantmaker_poc: + grantProviderPocName && grantProviderPocEmail + ? { POC_name: grantProviderPocName, POC_email: grantProviderPocEmail } + : { POC_name: "", POC_email: "" }, report_deadlines: reportDates as TDateISO[], timeline: timelineInYears, estimated_completion_time: estimatedCompletionTimeInHours, - description : description? description : "", + description: description ? description : "", attachments: attachments, - isRestricted: (isRestricted === "restricted"), + isRestricted: isRestricted === "restricted", }; - const result = grantToEdit + const result = grantToEdit ? await saveGrantEdits(grantData) : await createNewGrant(grantData); @@ -381,243 +445,477 @@ const [reportDates, setReportDates] = useState<(TDateISO | "")[]>( }; return ( - -
    {/*Greyed out background */} -
    {/*Popup container */} +
    + {" "} + {/*Greyed out background */} +
    + {" "} + {/*Popup container */}

    New Grant

    -
    {/* Major components in two columns */} +
    + {" "} + {/* Major components in two columns */} {/*left column */} -
    - {/*Organization name and input */} -
    - - + {/*Organization name and input */} +
    + + _setOrganization(e.target.value)}/> -
    + type="text" + placeholder="Type Here" + value={organization} + onChange={(e) => _setOrganization(e.target.value)} + /> +
    {/*Top left quadrant - from app date, start date, report deadlines, est completion time*/}
    - {/* Left column: Application + Grant Start row */} -
    - +
    {/*Application date and grant start date */}
    {/*Application date and input */}
    -
    {/*Grant Start Date and input */}
    -
    {/*Estimated completition time and input - need to make wider (length of application date and grant start date)*/} -
    -
    {/*Right column*/} -
    +
    {/*Report deadlines label and grey box */} -
    - -
    - {reportDates.map((date, index) => ( -
    - { - const newDates = [...reportDates]; - newDates[index] = e.target.value as TDateISO | ""; - setReportDates(newDates); +
    + +
    + + {reportDates.map((date, index) => ( +
    + { + const newDates = [...reportDates]; + newDates[index] = e.target.value as TDateISO | ""; + setReportDates(newDates); + }} + /> + {reportDates.length > 0 && ( + - )} + className="font-family-helvetica w-5 flex-shrink-0 rounded text-white font-bold flex items-center justify-center" + onClick={() => _removeReportDate(index)} + > + ✕ + + )}
    - - ))} - -
    + ))} +
    -
    +
    {/*Timeline label and input */} -
    -
    - {/*Right column */} -
    - +
    {/*POC row */} -
    +
    {/*BCAN POC div*/}
    - - {/*Box div*/} -
    - -
    - setBcanPocName(e.target.value)}/> - setBcanPocEmail(e.target.value)}/> -
    + + {/*Box div*/} +
    + +
    + setBcanPocName(e.target.value)} + /> + setBcanPocEmail(e.target.value)} + />
    +
    {/*Grant Provider POC div*/}
    - - {/*Box div*/} -
    - -
    - setGrantProviderPocName(e.target.value)}/> - setGrantProviderPocEmail(e.target.value)}/> -
    + + {/*Box div*/} +
    + +
    + setGrantProviderPocName(e.target.value)} + /> + setGrantProviderPocEmail(e.target.value)} + />
    +
    {/*bottom right row*/} -
    - {/* Select option menus */} -
    - {/*Qualify label and input */} -
    - - -
    +
    + {/* Select option menus */} +
    + {/*Qualify label and input */} +
    + + +
    - {/*Status label and input */} -
    - - -
    + {/*Status label and input */} +
    + + +
    - {/*Restriction types label and input */} -
    - - -
    + {/*Restriction types label and input */} +
    + +
    +
    {/*Scope Documents div p-2 h-full w-1/2 flex-col*/}
    -
    )} {/* Gray box showing added links */}
    {attachments .filter((a) => a.url) // show only filled ones .map((attachment, index) => ( -
    +
    ( borderStyle: "solid", borderColor: "black", borderWidth: "1px", - borderRadius: "1.2rem", }} - className="overflow-hidden font-family-helvetica flex-1 min-w-0 text-gray-700 rounded flex items-center px-3 justify-between" + className="overflow-hidden rounded-md font-family-helvetica flex-1 min-w-0 text-gray-700 rounded flex items-center px-3 justify-between" > ( {attachment.attachment_name || "Untitled"} - ({attachment.type === AttachmentType.SCOPE_DOCUMENT + ( + {attachment.type === AttachmentType.SCOPE_DOCUMENT ? "Scope" : "Supporting"} )
    ))}
    - {/*End bottom right row */} + {/*End bottom right row */}
    - {/*End right column */} + {/*End right column */}
    - - {/*End grid content*/} + {/*End grid content*/}
    - {/*Description and input */} -
    - -