diff --git a/package-lock.json b/package-lock.json index e024c5c..c42f654 100644 --- a/package-lock.json +++ b/package-lock.json @@ -902,9 +902,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001551", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001551.tgz", - "integrity": "sha512-vtBAez47BoGMMzlbYhfXrMV1kvRF2WP/lqiMuDu1Sb4EE4LKEgjopFDSRtZfdVnslNRpOqV/woE+Xgrwj6VQlg==", + "version": "1.0.30001611", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001611.tgz", + "integrity": "sha512-19NuN1/3PjA3QI8Eki55N8my4LzfkMCRLgCVfrl/slbSAchQfV0+GwjPrK3rq37As4UCLlM/DHajbKkAqbv92Q==", "funding": [ { "type": "opencollective", diff --git a/src/app/addNewClient/addNewClient.module.css b/src/app/addNewClient/addNewClient.module.css index 6f44459..8156500 100644 --- a/src/app/addNewClient/addNewClient.module.css +++ b/src/app/addNewClient/addNewClient.module.css @@ -1,71 +1,76 @@ +.container { + margin-left: 30px; +} + .header { - font-family: "Avenir"; - color: #000000; - } - - .subheader { - font-family: "Avenir"; - color: rgb(103, 109, 78) - } + font-family: "Avenir"; + color: #000000; +} - .insideSubheader { - font-family: "Avenir"; - color: black - } +.subheader { + font-family: "Avenir"; + color: rgb(103, 109, 78); +} - /* for boxes to appear side by side */ - .inputGroup { - display: inline-block; - margin-bottom: 5px; - padding: 5px; - } +.insideSubheader { + font-family: "Avenir"; + color: black; +} - /* entire input field with labels and input box */ - .inputContainer { - display: flex; - flex-direction: row; - } +/* for boxes to appear side by side */ +.inputGroup { + display: inline-block; + margin-bottom: 5px; + padding: 5px; +} - .inputBar { - flex: 1; - - margin-bottom: 20px; - padding-top: 15px; - padding-bottom: 15px; - padding-left: 60px; - padding-right: 60px; - font-family: "Avenir", sans-serif; - background-color: white; - border-width: 1px; - border-color: rgb(94, 161, 94); - border-radius: 3%; - } +/* entire input field with labels and input box */ +.inputContainer { + display: flex; + flex-direction: row; +} - .inputLabel { - display: block; - text-align: left; - margin-bottom: 3px; - font-size: 15px; - font-family: "Avenir", sans-serif; - } +.inputBar { + flex: 1; - .radioButton { - display: flex; - flex-direction: row; - margin-bottom: 5px; - } + margin-bottom: 20px; + padding-top: 15px; + padding-bottom: 15px; + padding-left: 60px; + padding-right: 60px; + font-family: "Avenir", sans-serif; + background-color: white; + border-width: 1px; + border-color: rgb(94, 161, 94); + border-radius: 3%; +} - .radioLabel { - font-family: "Avenir", sans-serif; - color: black; - } +.inputLabel { + display: block; + text-align: left; + margin-bottom: 3px; + font-size: 15px; + font-family: "Avenir", sans-serif; +} + +.radioButton { + display: flex; + flex-direction: row; + margin-bottom: 30px; +} + +.radioLabel { + font-family: "Avenir", sans-serif; + color: black; - .addButton { - border: 2px; - color: white; - border-radius: 3px; - font-family: "Avenir", sans-serif; - background: rgb(103, 109, 78); - width: 8em; - height: 3em; - } \ No newline at end of file +} + +.addButton { + border: 2px; + color: white; + border-radius: 3px; + font-family: "Avenir", sans-serif; + background: rgb(103, 109, 78); + width: 8em; + height: 3em; +} diff --git a/src/app/addNewClient/page.tsx b/src/app/addNewClient/page.tsx index fc951a2..d082e01 100644 --- a/src/app/addNewClient/page.tsx +++ b/src/app/addNewClient/page.tsx @@ -1,161 +1,345 @@ "use client"; -import Navbar from "../components/Navbar"; -import styles from "./addNewClient.module.css" -import { useState } from 'react'; +import Navbar from "@components/Navbar"; +import NewClientFields from "@components/NewClientFields"; +import styles from "./addNewClient.module.css"; +import { useEffect, useState, ChangeEvent } from "react"; -export default function AddNewClient() { -const [ - selectedValue, - setSelectedValue, -] = useState("option1"); - -const handleRadioChange = (value: any) => { - setSelectedValue(value); -}; - -const [category, setCategory] = useState(''); -const handleCategoryChange = (category: any) => { - setCategory(category); - console.log(category); +interface Member { + firstName: string; + lastName: string; + birthDate: string; + isAdult: boolean; + headHousehold: string; +} + +interface AuthMem { + firstName: string; + lastName: string; + birthDate: string; + client: string; } +interface FormData { + firstName: string; + lastName: string; + birthDate: string; + entryDates: Date[]; + authMem: string[]; + householdMem: string[]; + phoneNumber: string; + email: string; + address: string; + isFlagged: boolean; +} + +export default function AddNewClient() { + const [selectedValue, setSelectedValue] = useState(false); + const [numChildren, setNumChildren] = useState(0); + const [numAdults, setNumAdults] = useState(1); + const [householdMem, setHouseholdMem] = useState([]); + const [authMem, setAuthMem] = useState({ + firstName: "", + lastName: "", + birthDate: "", + client: "", + }); + + const [formData, setFormData] = useState({ + firstName: "", + lastName: "", + birthDate: "", + entryDates: [], + authMem: [], + householdMem: [], + phoneNumber: "", + email: "", + address: "", + isFlagged: false, + }); + + const handleChange = (e: ChangeEvent) => { + const value = e.target.value; + setFormData({ ...formData, [e.target.name]: value }); + //console.log(formData); + }; + + const handleMemChange = ( + e: ChangeEvent, + index: number, + isAdult: boolean + ) => { + const { name, value } = e.target; + setHouseholdMem((prev) => + prev.map((mem, idx) => { + if (mem.isAdult === isAdult && idx === index) { + return { ...mem, [name]: value }; + } + return mem; + }) + ); + + //console.log(householdMem); + }; + + const handleAuthMem = (e: ChangeEvent) => { + const { value, name } = e.target; + setAuthMem({ ...authMem, [name]: value }); + //console.log(authMem); + }; + + useEffect(() => { + setHouseholdMem((prev) => { + let children = prev.filter((mem) => !mem.isAdult).slice(0, numChildren); + let adults = prev.filter((mem) => mem.isAdult).slice(0, numAdults - 1); + + while (children.length < numChildren) { + children.push({ + firstName: "", + lastName: "", + birthDate: "", + isAdult: false, + headHousehold: "", + }); + } + while (adults.length < numAdults - 1) { + adults.push({ + firstName: "", + lastName: "", + birthDate: "", + isAdult: true, + headHousehold: "", + }); + } + + return [...children, ...adults]; + }); + }, [numChildren, numAdults]); + + // when form is submitted: + // 1. client is posted to db, its mongodb-geenrated id is then added to the householdMem array + // 2. householdMembers are posted to db, their mongodb-generated id's are returned and stored + // 3. the client is updated to hold the ids of their householdMembers + const handleSubmit = (e: React.FormEvent) => { + let headClientId: string; + e.preventDefault(); + + fetch("/api/clients", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(formData), + }) + .then((response) => { + return response.json(); + }) + .then((data) => { + headClientId = data.message; + //console.log(headClientId); + }) + .then(() => { + postMembers(headClientId); + }) + .then(() => { + postAuthMembers(headClientId); + }) + .catch((err) => console.log(err)); + }; + + const postMembers = (headClientId: string) => { + if (householdMem.length !== 0) { + householdMem.map((mem) => (mem.headHousehold = headClientId)); + + fetch("/api/householdMembers", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ members: householdMem }), + }) + .then((response) => { + return response.json(); + }) + .then((data) => { + const mems: string[] = []; + data.message.map((mem: any) => { + //console.log(mem._id); + mems.push(mem._id); + }); + + updateClientHousehold(mems, headClientId); + }) + .catch((err) => console.log(err)); + } + }; + + const updateClientHousehold = (mems: string[], clientId: string) => { + fetch(`/api/clients/${clientId}`, { + method: "PUT", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ householdMem: mems }), + }) + .then((response) => response.json()) + .then((data) => { + // log to console for testing purposes + console.log("client + household: ", data); + }) + .catch((err) => console.log(err)); + }; + + const updateClientAuth = (authMems: string[], clientId: string) => { + console.log(authMems); + fetch(`/api/clients/${clientId}`, { + method: "PUT", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ authorizedMem: authMems }), + }) + .then((response) => response.json()) + .then((data) => { + // log to console for testing purposes + console.log("client + auth: ", data); + }) + .catch((err) => console.log(err)); + }; + + const postAuthMembers = (clientId: string) => { + if (selectedValue) { + const authMemArr: string[] = []; + authMem.client = clientId; + fetch("/api/authorizedMembers", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(authMem), + }) + .then((response) => response.json()) + .then((data) => { + console.log("auth mem: ", data.message); + authMemArr.push(data.message); + //console.log(authMemArr); + updateClientAuth(authMemArr, clientId); + }) + .catch((err) => console.log(err)); + } + }; + + const handleRadioChange = () => { + setSelectedValue(!selectedValue); + }; + return ( <> -
-

Add New Client

-

Head of Household Information

-
-
- - - -
-
- - - -
-
-
- - - -
-
-
-

Household Information

-

Household Size

+
-
- - -
-
- - -
-
-

Adult 1

+

Add New Client

+

Head of Household Information

+ +
+
+

Household Information

+

Household Size

- - - + +
- - - -
-
- - - -
+ + +
-

Child 1

-
- - - -
+ {householdMem + .filter((mem) => mem.isAdult) + .map((member, index) => ( +
+

Adult {index + 2}

+ handleMemChange(e, index, true)} + /> +
+ ))} + + {householdMem + .filter((mem) => !mem.isAdult) + .map((member, index) => ( +
+

Child {index + 1}

+ handleMemChange(e, index, false)} + /> +
+ ))}

Additional Information

-
- + - handleRadioChange("option1") - } - /> - {/* for now, the button is set to being checked always */} + checked={selectedValue} + onClick={handleRadioChange} // handle state with onClick (as opposed to onChange) so users can unclick if they change their minds + onChange={() => {}} // no-op onChange field to keep react happy - react expects this when checked is controlled + /> + +
-

Authorized pick-up

-
- - - -
-
- - - -
-
- - - -
-
- + {selectedValue ? ( + <> +

Authorized pick-up

+ + + ) : ( + "" + )} + + + ); } diff --git a/src/app/api/authorizedMembers/route.ts b/src/app/api/authorizedMembers/route.ts new file mode 100644 index 0000000..12a013d --- /dev/null +++ b/src/app/api/authorizedMembers/route.ts @@ -0,0 +1,57 @@ +import { NextRequest, NextResponse } from "next/server"; +import connectDB from "../../../database/db"; +import AuthorizedMember from "../../../database/authorizedMemberSchema"; + +export async function GET(req: NextRequest) { + try { + // Establishing database connection + await connectDB(); + + // Fetching clients + const members = await AuthorizedMember.find(); + + // Checking if clients are found + if (members.length === 0) { + return NextResponse.json( + { message: "No authorized members found." }, + { status: 200 } + ); + } + + // Returning clients if found + return NextResponse.json(members); + } catch (err) { + console.error("Error fetching authorized members:", err); + return NextResponse.json( + { error: "Error fetching authorized members." }, + { status: 500 } + ); + } +} + +export async function POST(req: NextRequest) { + await connectDB(); + + + try { + const newAuthMemData = await req.json(); + + //create a new client using the data in body + const newAuthMem = new AuthorizedMember(newAuthMemData); + + //save the new client to the database + await newAuthMem.save(); + + return NextResponse.json( + { message: newAuthMem._id.valueOf() }, + { status: 200 } + ); + } catch (err) { + //if unable to add authorized member, return error + console.log("Error adding authorized member:", err); + return NextResponse.json( + { error: "Error adding authorized member." }, + { status: 500 } + ); + } +} diff --git a/src/app/api/clients/[userId]/route.ts b/src/app/api/clients/[userId]/route.ts index 5b3e93a..682e858 100644 --- a/src/app/api/clients/[userId]/route.ts +++ b/src/app/api/clients/[userId]/route.ts @@ -48,6 +48,7 @@ export async function DELETE(req: NextRequest, { params }: IParams) { await connectDB(); const { userId } = params; + try { //find the client to delete by the given id const client = await IClientSchema.findOne({ _id: userId }); @@ -65,3 +66,51 @@ export async function DELETE(req: NextRequest, { params }: IParams) { ); } } + +// update a client's household members +export async function PUT(req: NextRequest, { params }: IParams) { + await connectDB(); + const { userId } = params; + try { + const body = await req.json(); + + const updateData: any = {}; + // Dynamically add fields to updateData if they exist in the request body + if (body.householdMem) { + updateData.householdMem = body.householdMem; + } + if (body.authorizedMem) { + updateData.authorizedMem = body.authorizedMem; + } + + let updatedClient; + + if (updateData.householdMem) { + updatedClient = await IClientSchema.findByIdAndUpdate( + userId, + { householdMem: updateData.householdMem}, + { new: true } + ); + } + if (updateData.authorizedMem) { + updatedClient = await IClientSchema.findByIdAndUpdate( + userId, + { authMem: updateData.authorizedMem}, + { new: true } + ); + } + + if (!updatedClient) { + return NextResponse.json({ error: "Client not found." }, { status: 404 }); + } + + return NextResponse.json(updatedClient, { status: 200 }); + } catch (err) { + console.error("Error updating client:", err); + return NextResponse.json( + { error: "Error updating client." }, + { status: 500 } + ); + } +} + diff --git a/src/app/api/clients/route.ts b/src/app/api/clients/route.ts index e4e7621..7ab0033 100644 --- a/src/app/api/clients/route.ts +++ b/src/app/api/clients/route.ts @@ -37,12 +37,12 @@ export async function POST(req: NextRequest) { //create a new client using the data in body const newClient = new IClientSchema(newClientData); - console.log(newClient); //save the new client to the database await newClient.save(); + return NextResponse.json( - { message: "Operation successful" }, + { message: newClient._id.valueOf() }, { status: 200 } ); } catch (err) { diff --git a/src/app/api/householdMembers/route.ts b/src/app/api/householdMembers/route.ts new file mode 100644 index 0000000..6bdcc83 --- /dev/null +++ b/src/app/api/householdMembers/route.ts @@ -0,0 +1,54 @@ +import { NextRequest, NextResponse } from "next/server"; +import connectDB from "../../../database/db"; +import HouseholdMember from "../../../database/householdMemberSchema"; + +export async function GET(req: NextRequest) { + try { + // Establishing database connection + await connectDB(); + + // Fetching clients + const members = await HouseholdMember.find(); + + // Checking if clients are found + if (members.length === 0) { + return NextResponse.json( + { message: "No clients found." }, + { status: 200 } + ); + } + + // Returning clients if found + return NextResponse.json(members); + } catch (err) { + console.error("Error fetching members:", err); + return NextResponse.json( + { error: "Error fetching members." }, + { status: 500 } + ); + } +} + +export async function POST(req: NextRequest) { + await connectDB(); + + try { + const { members } = await req.json(); + if (!Array.isArray(members)) { + return NextResponse.json( + { message: "Expected array of household members." }, + { status: 400 } + ); + } + + const result = await HouseholdMember.insertMany(members); + return NextResponse.json({ message: result }, { status: 200 }); + } catch (err) { + //if unable to add client, return error + console.log("Error adding members:", err); + return NextResponse.json( + { error: "Error adding members." }, + { status: 500 } + ); + } +} diff --git a/src/app/components/NewClientFields.css b/src/app/components/NewClientFields.css new file mode 100644 index 0000000..15294b4 --- /dev/null +++ b/src/app/components/NewClientFields.css @@ -0,0 +1,28 @@ +.inputGroup { + display: inline-block; + margin-bottom: 5px; + padding: 5px; +} + +.inputBar { + flex: 1; + + margin-bottom: 20px; + padding-top: 15px; + padding-bottom: 15px; + padding-left: 60px; + padding-right: 60px; + font-family: "Avenir", sans-serif; + background-color: white; + border-width: 1px; + border-color: rgb(94, 161, 94); + border-radius: 3%; +} + +.inputLabel { + display: block; + text-align: left; + margin-bottom: 3px; + font-size: 15px; + font-family: "Avenir", sans-serif; +} diff --git a/src/app/components/NewClientFields.tsx b/src/app/components/NewClientFields.tsx new file mode 100644 index 0000000..0d5ee30 --- /dev/null +++ b/src/app/components/NewClientFields.tsx @@ -0,0 +1,96 @@ +import React, { ChangeEvent } from "react"; +import "./NewClientFields.css"; + +interface Props { + onAction: (e: ChangeEvent) => void; + isClient: boolean; +} + +const NewClientFields = ({ onAction, isClient }: Props) => { + return ( +
+
+ +
+
+ +
+
+ +
+ {isClient ? ( +
+
+ +
+
+ +
+
+ +
+
+ ) : ( + "" + )} +
+ ); +}; + +export default NewClientFields;