diff --git a/backend/src/controllers/vsr.ts b/backend/src/controllers/vsr.ts index eaecd7c..db2c37d 100644 --- a/backend/src/controllers/vsr.ts +++ b/backend/src/controllers/vsr.ts @@ -34,18 +34,20 @@ export const createVSR: RequestHandler = async (req, res, next) => { employmentStatus, incomeLevel, sizeOfHome, - hearFrom, - petCompanion, - militaryId, - lastRank, - serviceConnected, - dischargeStatus, - email, - phoneNumber, - zipCode, - state, - city, streetAddress, + city, + state, + zipCode, + phoneNumber, + email, + branch, + conflicts, + dischargeStatus, + serviceConnected, + lastRank, + militaryID, + petCompanion, + hearFrom, selectedFurnitureItems, additionalItems, } = req.body; @@ -69,6 +71,20 @@ export const createVSR: RequestHandler = async (req, res, next) => { employmentStatus, incomeLevel, sizeOfHome, + streetAddress, + city, + state, + zipCode, + phoneNumber, + email, + branch, + conflicts, + dischargeStatus, + serviceConnected, + lastRank, + militaryID, + petCompanion, + hearFrom, // Use current date as timestamp for received & updated dateReceived: currentDate, @@ -76,18 +92,6 @@ export const createVSR: RequestHandler = async (req, res, next) => { status: "Received", - hearFrom, - petCompanion, - militaryId, - lastRank, - serviceConnected, - dischargeStatus, - email, - phoneNumber, - zipCode, - state, - city, - streetAddress, selectedFurnitureItems, additionalItems, }); diff --git a/backend/src/models/vsr.ts b/backend/src/models/vsr.ts index bcbb228..b6a949d 100644 --- a/backend/src/models/vsr.ts +++ b/backend/src/models/vsr.ts @@ -7,16 +7,16 @@ const furntitureInputSchema = new Schema({ const vsrSchema = new Schema({ name: { type: String, required: true }, - gender: { type: String, require: true }, - age: { type: Number, require: true }, + gender: { type: String, required: true }, + age: { type: Number, required: true }, maritalStatus: { type: String, required: true }, spouseName: { type: String }, agesOfBoys: { type: [Number] }, agesOfGirls: { type: [Number] }, - ethnicity: { type: [String], require: true }, - employmentStatus: { type: String, require: true }, - incomeLevel: { type: String, require: true }, - sizeOfHome: { type: String, require: true }, + ethnicity: { type: [String], required: true }, + employmentStatus: { type: String, required: true }, + incomeLevel: { type: String, required: true }, + sizeOfHome: { type: String, required: true }, streetAddress: { type: String, required: true }, city: { type: String, required: true }, state: { type: String, required: true }, @@ -26,10 +26,10 @@ const vsrSchema = new Schema({ branch: { type: [String], required: true }, conflicts: { type: [String], required: true }, dischargeStatus: { type: String, required: true }, - serviceConnected: { type: String, required: true }, + serviceConnected: { type: Boolean, required: true }, lastRank: { type: String, required: true }, - militaryId: { type: Number, required: true }, - petCompanion: { type: String, required: true }, + militaryID: { type: Number, required: true }, + petCompanion: { type: Boolean, required: true }, hearFrom: { type: String, required: true }, selectedFurnitureItems: { type: [furntitureInputSchema], required: true }, additionalItems: { type: String, required: false }, diff --git a/backend/src/validators/vsr.ts b/backend/src/validators/vsr.ts index 6d1bbdf..e332037 100644 --- a/backend/src/validators/vsr.ts +++ b/backend/src/validators/vsr.ts @@ -82,65 +82,40 @@ const makeSizeOfHomeValidator = () => .isString() .withMessage("Size of Home must be a string"); -const ALLOWED_STATUSES = [ - "Received", - "Appointment Scheduled", - "Approved", - "Resubmit", - "No-show / Incomplete", - "Archived", -]; - -const updateStatusValidator = () => - body("status") +const makeStreetAddressValidator = () => + body("streetAddress") .exists({ checkFalsy: true }) - .withMessage("Status is required") + .withMessage("Address is required") .isString() - .withMessage("Status must be a string") - .isIn(ALLOWED_STATUSES) - .withMessage("Status must be one of the allowed options"); + .withMessage("Address must be a string"); -const makeHearFromValidator = () => - body("hearFrom") +const makeCityValidator = () => + body("city") .exists({ checkFalsy: true }) - .withMessage("Hear from is required") + .withMessage("City is required") .isString() - .withMessage("Hear from must be a string"); - -const makePetCompanionValidator = () => - body("petCompanion") - .exists({ checkFalsy: true }) - .withMessage("Pet companion is required") - .isBoolean() - .withMessage("Pet companion must be a boolean"); - -const makeMilitaryIdValidator = () => - body("militaryId") - .exists({ checkFalsy: true }) - .withMessage("Military Id is required") - .isInt() - .withMessage("Military Id must be an integer"); + .withMessage("City must be a string"); -const makeLastRankValidator = () => - body("lastRank") +const makeStateValidator = () => + body("state") .exists({ checkFalsy: true }) - .withMessage("Last rank is required") + .withMessage("State is required") .isString() - .withMessage("Last rank must be a string"); + .withMessage("State must be a string"); -const makeServiceConnectedValidator = () => - body("serviceConnected") +const makeZipCodeValidator = () => + body("zipCode") .exists({ checkFalsy: true }) - .withMessage("Service connected is required") - .isBoolean() - .withMessage("Service connected must be a boolean"); + .withMessage("Zip Code is required") + .isInt({ min: 10000 }) + .withMessage("Zip Code must be a 5 digit integer"); -const makeDischargeStatusValidator = () => - body("dischargeStatus") +const makePhoneNumberValidator = () => + body("phoneNumber") .exists({ checkFalsy: true }) - .withMessage("Discharge status is required") + .withMessage("Phone Number is required") .isString() - .withMessage("Discharge status must be a string"); + .withMessage("Phone number must be a string"); const makeEmailValidator = () => body("email") @@ -149,40 +124,83 @@ const makeEmailValidator = () => .isString() .withMessage("Email must be a string"); -const makePhoneNumberValidator = () => - body("phoneNumber") +const makeBranchValidator = () => + body("branch") .exists({ checkFalsy: true }) - .withMessage("Phone number is required") - .isString() - .withMessage("Phone number must be a string"); + .withMessage("Branch is required") + .isArray() + .withMessage("Branch must be an array") + .custom((branches: string[]) => branches.every((branch) => typeof branch == "string")) + .withMessage("Each branch must be a string"); -const makeZipCodeValidator = () => - body("zipCode") +const makeConflictsValidator = () => + body("conflicts") .exists({ checkFalsy: true }) - .withMessage("Zip code is required") - .isInt({ min: 0 }) - .withMessage("Zip code must be a positive integer"); + .withMessage("Conflict(s) is required") + .isArray() + .withMessage("Conflict(s) must be an array") + .custom((conflicts: string[]) => conflicts.every((conflict) => typeof conflict === "string")) + .withMessage("Each conflict must be a string"); -const makeStateValidator = () => - body("state") +const makeDischargeStatusValidator = () => + body("dischargeStatus") .exists({ checkFalsy: true }) - .withMessage("State is required") + .withMessage("Discharge Status is required") .isString() - .withMessage("State must be a string"); + .withMessage("Discharge Status must be a string"); -const makeCityValidator = () => - body("city") +const makeServiceConnectedValidator = () => + body("serviceConnected") + .exists({ checkFalsy: false }) + .withMessage("Service Connected is required") + .isBoolean() + .withMessage("Service Connected must be a boolean"); + +const makeLastRankValidator = () => + body("lastRank") .exists({ checkFalsy: true }) - .withMessage("City is required") + .withMessage("Last rank is required") .isString() - .withMessage("City must be a string"); + .withMessage("Last rank must be a string"); -const makeStreetAddressValidator = () => - body("streetAddress") +const makeMilitaryIDValidator = () => + body("militaryID") + .exists({ checkFalsy: true }) + .withMessage("Military ID is required") + .isInt() + .withMessage("Military ID must be an integer"); + +const makePetCompanionValidator = () => + body("petCompanion") + .exists({ checkFalsy: false }) + .withMessage("Pet interest is required") + .isBoolean() + .withMessage("Pet interest must be a boolean"); + +const makeHearFromValidator = () => + body("hearFrom") + .exists({ checkFalsy: true }) + .withMessage("Referral source is required") + .isString() + .withMessage("Referral source must be a string"); + +const ALLOWED_STATUSES = [ + "Received", + "Appointment Scheduled", + "Approved", + "Resubmit", + "No-show / Incomplete", + "Archived", +]; + +const updateStatusValidator = () => + body("status") .exists({ checkFalsy: true }) - .withMessage("Street address is required") + .withMessage("Status is required") .isString() - .withMessage("Street address must be a string"); + .withMessage("Status must be a string") + .isIn(ALLOWED_STATUSES) + .withMessage("Status must be one of the allowed options"); export const createVSR = [ makeNameValidator(), @@ -196,18 +214,20 @@ export const createVSR = [ makeEmploymentStatusValidator(), makeIncomeLevelValidator(), makeSizeOfHomeValidator(), - makeHearFromValidator(), - makePetCompanionValidator(), - makeMilitaryIdValidator(), - makeLastRankValidator(), - makeServiceConnectedValidator(), - makeDischargeStatusValidator(), - makeEmailValidator(), - makePhoneNumberValidator(), - makeZipCodeValidator(), - makeStateValidator(), - makeCityValidator(), makeStreetAddressValidator(), + makeCityValidator(), + makeStateValidator(), + makeZipCodeValidator(), + makePhoneNumberValidator(), + makeEmailValidator(), + makeBranchValidator(), + makeConflictsValidator(), + makeDischargeStatusValidator(), + makeServiceConnectedValidator(), + makeLastRankValidator(), + makeMilitaryIDValidator(), + makePetCompanionValidator(), + makeHearFromValidator(), ]; export const updateStatus = [updateStatusValidator()]; diff --git a/frontend/src/api/VSRs.ts b/frontend/src/api/VSRs.ts index 0c79c11..035f41b 100644 --- a/frontend/src/api/VSRs.ts +++ b/frontend/src/api/VSRs.ts @@ -29,7 +29,7 @@ export interface VSRJson { dischargeStatus: string; serviceConnected: boolean; lastRank: string; - militaryId: number; + militaryID: number; petCompanion: boolean; selectedFurnitureItems: FurnitureInput[]; additionalItems: string; @@ -67,7 +67,7 @@ export interface VSR { dischargeStatus: string; serviceConnected: boolean; lastRank: string; - militaryId: number; + militaryID: number; petCompanion: boolean; selectedFurnitureItems: FurnitureInput[]; additionalItems: string; @@ -100,11 +100,11 @@ export interface CreateVSRRequest { dischargeStatus: string; serviceConnected: boolean; lastRank: string; - militaryId: number; + militaryID: number; petCompanion: boolean; + hearFrom: string; selectedFurnitureItems: FurnitureInput[]; additionalItems: string; - hearFrom: string; } function parseVSR(vsr: VSRJson) { @@ -132,7 +132,7 @@ function parseVSR(vsr: VSRJson) { dischargeStatus: vsr.dischargeStatus, serviceConnected: vsr.serviceConnected, lastRank: vsr.lastRank, - militaryId: vsr.militaryId, + militaryID: vsr.militaryID, petCompanion: vsr.petCompanion, selectedFurnitureItems: vsr.selectedFurnitureItems, additionalItems: vsr.additionalItems, diff --git a/frontend/src/app/vsr/page.module.css b/frontend/src/app/vsr/page.module.css index 6ad3837..c520a45 100644 --- a/frontend/src/app/vsr/page.module.css +++ b/frontend/src/app/vsr/page.module.css @@ -90,6 +90,11 @@ color: var(--Secondary-2, #be2d46); } +.buttonContainer { + display: flex; + justify-content: space-between; +} + .submitButton { align-self: end; } @@ -108,6 +113,32 @@ border: none; border-radius: 4px; } +.enabled { + background-color: #102d5f; +} + +.disabled { + background-color: grey; +} + +.backButton { + align-self: start; +} + +.back { + width: 306px; + height: 64px; + background-color: white; + color: #102d5f; + cursor: pointer; + font-family: Lora; + font-size: 24px; + font-style: normal; + font-weight: 700; + line-height: normal; + border: #102d5f; + border-radius: 4px; +} .sectionHeader { gap: 4px; @@ -198,7 +229,6 @@ .submit { width: 306px; height: 64px; - background-color: #102d5f; color: white; cursor: pointer; font-family: "Lora"; diff --git a/frontend/src/app/vsr/page.tsx b/frontend/src/app/vsr/page.tsx index e013124..d1d724f 100644 --- a/frontend/src/app/vsr/page.tsx +++ b/frontend/src/app/vsr/page.tsx @@ -9,6 +9,7 @@ import HeaderBar from "@/components/shared/HeaderBar"; import PageNumber from "@/components/VSRForm/PageNumber"; import { createVSR, CreateVSRRequest, FurnitureInput } from "@/api/VSRs"; import { FurnitureItem, getFurnitureItems } from "@/api/FurnitureItems"; +import BinaryChoice from "@/components/shared/input/BinaryChoice"; import { FurnitureItemSelection } from "@/components/VeteranForm/FurnitureItemSelection"; interface IFormInput { @@ -47,12 +48,20 @@ const VeteranServiceRequest: React.FC = () => { register, handleSubmit, control, - formState: { errors }, + formState: { errors, isValid }, watch, } = useForm(); const [selectedEthnicities, setSelectedEthnicities] = useState([]); const [otherEthnicity, setOtherEthnicity] = useState(""); + const [selectedConflicts, setSelectedConflicts] = useState([]); + const [otherConflict, setOtherConflict] = useState(""); + + const [selectedBranch, setSelectedBranch] = useState([]); + + const [selectedHearFrom, setSelectedHearFrom] = useState(""); + const [otherHearFrom, setOtherHearFrom] = useState(""); + const [pageNumber, setPageNumber] = useState(1); const numBoys = watch("num_boys"); @@ -97,6 +106,109 @@ const VeteranServiceRequest: React.FC = () => { "Prefer not to say", ]; + const branchOptions = [ + "Air Force", + "Air Force Reserve", + "Air National Guard", + "Army", + "Army Air Corps", + "Army Reserve", + "Coast Guard", + "Marine Corps", + "Navy", + "Navy Reserve", + ]; + + const conflictsOptions = [ + "WWII", + "Korea", + "Vietnam", + "Persian Gulf", + "Bosnia", + "Kosovo", + "Panama", + "Kuwait", + "Iraq", + "Somalia", + "Desert Shield/Storm", + "Operation Enduring Freedom (OEF)", + "Afghanistan", + "Irani Crisis", + "Granada", + "Lebanon", + "Beirut", + "Special Ops", + "Peacetime", + ]; + + const dischargeStatusOptions = [ + "Honorable Discharge", + "General Under Honorable", + "Other Than Honorable", + "Bad Conduct", + "Entry Level", + "Dishonorable", + "Still Serving", + "Civilian", + "Medical", + "Not Given", + ]; + + const hearFromOptions = ["Colleague", "Social Worker", "Friend", "Internet", "Social Media"]; + + const stateOptions = [ + "AL", + "AK", + "AZ", + "AR", + "CA", + "CO", + "CT", + "DE", + "FL", + "GA", + "HI", + "ID", + "IL", + "IN", + "IA", + "KS", + "KY", + "LA", + "ME", + "MD", + "MA", + "MI", + "MN", + "MS", + "MO", + "MT", + "NE", + "NV", + "NH", + "NJ", + "NM", + "NY", + "NC", + "ND", + "OH", + "OK", + "OR", + "PA", + "RI", + "SC", + "SD", + "TN", + "TX", + "UT", + "VT", + "VA", + "WA", + "WV", + "WI", + "WY", + ]; + const [errorMessage, setErrorMessage] = useState(null); const [furnitureCategoriesToItems, setFurnitureCategoriesToItems] = @@ -161,20 +273,22 @@ const VeteranServiceRequest: React.FC = () => { employmentStatus: data.employment_status, incomeLevel: data.income_level, sizeOfHome: data.size_of_home, - streetAddress: "1111 TSE Lane", - city: "San Diego", - state: "CA", - zipCode: 92122, - phoneNumber: "123-456-7890", - email: "tsepapdev@gmail.com", - branch: ["Navy", "Air Force"], - conflicts: [], - dischargeStatus: "Still Serving", - serviceConnected: true, - lastRank: "Officer", - militaryId: 2932, - petCompanion: true, - hearFrom: "Social Media", + + streetAddress: data.streetAddress, + city: data.city, + state: data.state, + zipCode: data.zipCode, + phoneNumber: data.phoneNumber, + email: data.email, + branch: selectedBranch, + conflicts: selectedConflicts.concat(otherConflict === "" ? [] : [otherConflict]), + dischargeStatus: data.dischargeStatus, + serviceConnected: data.serviceConnected, + lastRank: data.lastRank, + militaryID: data.militaryID, + petCompanion: data.petCompanion, + hearFrom: data.hearFrom, + // Only submit items that the user selected at least 1 of selectedFurnitureItems: Object.values(selectedFurnitureItems).filter( (selectedItem) => selectedItem.quantity > 0, @@ -494,7 +608,10 @@ const VeteranServiceRequest: React.FC = () => {
-
@@ -503,6 +620,357 @@ const VeteranServiceRequest: React.FC = () => { ); + } else if (pageNumber === 2) { + return ( +
+
+ +
+
+
+
+

Contact Information

+ +
+
+ +
+ +
+ +
+ +
+ ( + field.onChange(e)} + required + error={!!errors.state} + helperText={errors.state?.message} + placeholder="Select a state" + /> + )} + /> +
+ +
+ +
+
+ +
+
+ +
+ +
+ +
+
+
+
+
+ +
+
+
+

Military Background

+ + ( + { + const valueToSet = ((newValue as string[]) ?? [])[0] ?? ""; + if (valueToSet !== "") { + field.onChange(valueToSet); + } + setSelectedBranch(newValue as string[]); + }} + required + error={!!errors.branch} + helperText={errors.branch?.message} + allowMultiple + /> + )} + /> + + ( + <> + { + const valueToSet = ((newValue as string[]) ?? [])[0] ?? ""; + if (valueToSet !== "" || otherConflict === "") { + field.onChange(valueToSet); + } + setSelectedConflicts(newValue as string[]); + }} + required + error={!!errors.conflicts} + helperText={errors.conflicts?.message} + allowMultiple + /> +
+ { + const value = e.target.value; + if (value !== "" || selectedConflicts.length === 0) { + field.onChange(value); + } + setOtherConflict(value); + }} + variant={"outlined"} + required={false} + /> +
+ + )} + /> + + ( + field.onChange(newValue)} + required + error={!!errors.dischargeStatus} + helperText={errors.dischargeStatus?.message} + /> + )} + /> + + + [true, false].includes(value) || "Service connected is required", + }} + render={({ field }) => ( + field.onChange(newValue)} + required + error={!!errors.serviceConnected} + helperText={errors.serviceConnected?.message} + /> + )} + /> + +
+
+ +
+ +
+ +
+
+
+
+
+ +
+
+
+

Additional Information

+ + + [true, false].includes(value) || "Companionship animal is required", + }} + render={({ field }) => ( + field.onChange(newValue)} + required + error={!!errors.petCompanion} + helperText={errors.petCompanion?.message} + /> + )} + /> + + ( + <> + { + const valueToSet = Array.isArray(newValue) ? newValue[0] : newValue; + if (valueToSet !== "" || otherHearFrom === "") { + field.onChange(valueToSet); + } + setSelectedHearFrom(valueToSet); + }} + required + error={!!errors.hearFrom} + helperText={errors.hearFrom?.message} + /> +
+ { + const value = e.target.value; + if (value !== "") { + field.onChange(value); + } + setOtherHearFrom(value); + setSelectedHearFrom(""); + }} + variant={"outlined"} + required={false} + /> +
+ + )} + /> +
+
+
+
+
+ +
+ +
+ +
+
+
+ +
+ ); } else { return (
diff --git a/frontend/src/components/VSRIndividual/MilitaryBackground/index.tsx b/frontend/src/components/VSRIndividual/MilitaryBackground/index.tsx index 6edea5e..abd6d5e 100644 --- a/frontend/src/components/VSRIndividual/MilitaryBackground/index.tsx +++ b/frontend/src/components/VSRIndividual/MilitaryBackground/index.tsx @@ -36,7 +36,7 @@ export const MilitaryBackground = ({ vsr }: MilitaryBackgroundProps) => {
- +
); diff --git a/frontend/src/components/VSRTable/VSRTable/index.tsx b/frontend/src/components/VSRTable/VSRTable/index.tsx index 0de4f02..29d692e 100644 --- a/frontend/src/components/VSRTable/VSRTable/index.tsx +++ b/frontend/src/components/VSRTable/VSRTable/index.tsx @@ -34,7 +34,7 @@ const columns: GridColDef[] = [ hideSortIcons: true, }, { - field: "militaryId", + field: "militaryID", headerName: "Military ID (Last 4)", type: "string", flex: 1, diff --git a/frontend/src/components/shared/input/BinaryChoice/index.tsx b/frontend/src/components/shared/input/BinaryChoice/index.tsx new file mode 100644 index 0000000..a459430 --- /dev/null +++ b/frontend/src/components/shared/input/BinaryChoice/index.tsx @@ -0,0 +1,51 @@ +import { useState } from "react"; +import Chip from "@mui/material/Chip"; +import styles from "@/components/shared/input/BinaryChoice/styles.module.css"; + +export interface BinaryChoiceProps { + label: string; + value: boolean | null; + onChange: (selected: boolean | null) => void; + required: boolean; + error?: boolean; + helperText?: string; +} + +const BinaryChoice = ({ label, value, onChange, required, helperText }: BinaryChoiceProps) => { + const [selectedOption, setSelectedOption] = useState(value); + + const handleOptionClick = (newOption: boolean | null) => { + setSelectedOption(newOption); + onChange(newOption); + }; + + return ( +
+

+ {required ? * : null} + {label} +

+
+ handleOptionClick(true)} + className={`${styles.chip} ${ + selectedOption === true ? styles.chipSelected : styles.chipUnselected + }`} + clickable + /> + handleOptionClick(false)} + className={`${styles.chip} ${ + selectedOption === false ? styles.chipSelected : styles.chipUnselected + }`} + clickable + /> +
+ {helperText ?
{helperText}
: null} +
+ ); +}; + +export default BinaryChoice; diff --git a/frontend/src/components/shared/input/BinaryChoice/styles.module.css b/frontend/src/components/shared/input/BinaryChoice/styles.module.css new file mode 100644 index 0000000..2f69852 --- /dev/null +++ b/frontend/src/components/shared/input/BinaryChoice/styles.module.css @@ -0,0 +1,64 @@ +/* BinaryChoice.module.css */ + +.wrapperClass { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 16px; + flex: 1 0 0; + font-size: 16px; + color: var(--Light-Gray, #818181); + font-family: "Open Sans"; + font-style: normal; + font-weight: 400; + line-height: normal; +} + +.chip { + border-width: 1px; + border-style: solid; + text-align: center; + font-family: "Open Sans"; + font-size: 16px; + font-style: normal; + font-weight: 400; + line-height: normal; + border-radius: 64px; + border: 1px solid var(--Secondary-1, #102d5f); +} + +.chipSelected { + color: white; + background: #102d5f; +} + +.chipSelected:hover { + background: #102d5f; +} + +.chipContainer { + display: flex; + flex-direction: row; + gap: 16px; + flex-wrap: wrap; +} + +.chipUnselected { + color: var(--Accent-Blue-1, #102d5f); + background: rgba(255, 255, 255, 0); +} + +/*Need exact color for this*/ +.chipUnselected:hover { + background: rgb(213, 232, 239); +} + +.requiredAsterisk { + color: var(--Secondary-2, #be2d46); + text-align: left; +} + +.helperText { + color: var(--Secondary-2, #be2d46); + font-size: 12px; +}