From 467ad00eab576be4181173f1f2311a2172f5cb4a Mon Sep 17 00:00:00 2001 From: Ola Bekkevold Date: Fri, 20 Feb 2026 15:38:26 +0100 Subject: [PATCH 01/14] feat: loan and borrower editing add editing functionality to loans and borrowers which works pretty similar to how equipment editing is already implemented Refs: #46 --- labman/src/components/core/Card.tsx | 11 +- labman/src/components/core/CardList.tsx | 13 +- .../components/inventory/EquipmentInfo.tsx | 4 +- labman/src/components/inventory/LoanView.tsx | 12 +- labman/src/components/loans/EditLoan.tsx | 236 ++++++++++++++++++ labman/src/lib/actions.ts | 86 +++++-- 6 files changed, 334 insertions(+), 28 deletions(-) create mode 100644 labman/src/components/loans/EditLoan.tsx diff --git a/labman/src/components/core/Card.tsx b/labman/src/components/core/Card.tsx index 06907b6..da4f473 100644 --- a/labman/src/components/core/Card.tsx +++ b/labman/src/components/core/Card.tsx @@ -6,11 +6,13 @@ import {LoanClass} from "@/types/Loan"; interface CardProps { user?: UserClass loan?: LoanClass; + setSideView?: (view: string) => void; + setSelectedLoanId?: (id: number | null) => void; } // TODO: Could have a button to reactivate a loan, not a priority right now -export default function Card({ loan, user }: CardProps) { +export default function Card({ loan, user, setSelectedLoanId, setSideView }: CardProps) { let {name, start, last} = {name: "", start: "", last: ""}; @@ -67,7 +69,12 @@ export default function Card({ loan, user }: CardProps) { }
- + { loan && loan.status != "Returned" && }
diff --git a/labman/src/components/core/CardList.tsx b/labman/src/components/core/CardList.tsx index 04ba179..50cd954 100644 --- a/labman/src/components/core/CardList.tsx +++ b/labman/src/components/core/CardList.tsx @@ -5,6 +5,7 @@ import {User} from "@/generated/prisma"; import {UserClass} from "@/types/User"; import {returnLoan, deleteLoan, deleteUser} from "@/lib/actions"; import {LoanClass} from "@/types/Loan"; +import EditLoan from "@/components/loans/EditLoan"; type Loans = { @@ -51,6 +52,10 @@ export default function CardList({ loansProp = [], usersProp = []}: CardListProp const [username, setUsername] = useState(""); const [password, setPassword] = useState(""); + const [sideView, setSideView] = useState(""); + const [selectedLoanId, setSelectedLoanId] = useState(null); + + async function handleSubmit(e: React.FormEvent) { e.preventDefault(); const res = await fetch("/api/register", { @@ -118,6 +123,12 @@ export default function CardList({ loansProp = [], usersProp = []}: CardListProp return (
+ { sideView == "loanEdit" && selectedLoanId && loan.id === selectedLoanId)!} + setSideView={setSideView} + setLoans={setLoans} + + />} { users.length !== 0 &&
setUsername(e.target.value)} type="text" name="username" placeholder="Username" className="bg-white rounded-md p-2 m-2 placeholder-black text-black" /> @@ -140,7 +151,7 @@ export default function CardList({ loansProp = [], usersProp = []}: CardListProp returnLoan: async (id : number) => handleReturnLoan(id) } ) - return ; + return ; })} {users.map(userDto => { const user = new UserClass( diff --git a/labman/src/components/inventory/EquipmentInfo.tsx b/labman/src/components/inventory/EquipmentInfo.tsx index 864de91..6bfdc80 100644 --- a/labman/src/components/inventory/EquipmentInfo.tsx +++ b/labman/src/components/inventory/EquipmentInfo.tsx @@ -91,7 +91,7 @@ export default function EquipmentInfo({equipmentData, setSideView, setAllEquipme const updatedEq = await updateEquipment(equipmentData!.id, formData.name!, formData.category!, formData.image!) const updatedEquipment = { - ...equipmentData!, + ...equipmentData, name: updatedEq.name, category: { id: updatedEq.category.id, @@ -128,7 +128,7 @@ export default function EquipmentInfo({equipmentData, setSideView, setAllEquipme

Equipment information

- +
diff --git a/labman/src/components/inventory/LoanView.tsx b/labman/src/components/inventory/LoanView.tsx index 9f73816..64f90ff 100644 --- a/labman/src/components/inventory/LoanView.tsx +++ b/labman/src/components/inventory/LoanView.tsx @@ -110,11 +110,14 @@ export default function LoanView({setSideView, equipmentData, setAllEquipment, s type="text" className="side-form-input" onChange={(e) => { - console.log(e.target.value); - setFormData({...formData, borrower: e.target.value}) - console.log(formData); const borrower = borrowers.find(borrower => borrower.name === e.target.value); - if (borrower) setFormData({...formData, borrowerPhone: borrower.phone, borrowerEmail: borrower.email}); + // TODO: Better to use functional updates because React hasn't re-rendered the component yet and should not do two state changes at once due to batching + setFormData(prev => ({ + ...prev, + borrower: e.target.value, + borrowerPhone: borrower?.phone || "", + borrowerMail: borrower?.email || "" + })) }} /> @@ -127,7 +130,6 @@ export default function LoanView({setSideView, equipmentData, setAllEquipment, s type="date" required value={formData.startDate} - min={today} className="side-form-input" onChange={(e) => { const selected = e.target.value; diff --git a/labman/src/components/loans/EditLoan.tsx b/labman/src/components/loans/EditLoan.tsx new file mode 100644 index 0000000..97ddbf6 --- /dev/null +++ b/labman/src/components/loans/EditLoan.tsx @@ -0,0 +1,236 @@ +"use client" + +import {loanCount} from "@/utils/inventoryUtils"; +import {useEffect, useState} from "react"; +import {updateLoan} from "@/lib/actions"; + +type Loan = { + id: number; + startDate: Date; + endDate: Date; + status: string; + + borrower: { + id: number; + name: string; + phone?: string | null; + email?: string | null + note?: string | null + creationDate: Date; + + } + item: { + id: number; + equipment: { + id: number; + name: string; + categoryId: number; + image: string | null; + createdAt: Date; + + } + } +} + +type Borrower = { + id: number; + name: string; + phone: string; + email: string; +} + +interface EditLoanProps { + loan: Loan; + setSideView: (view: string) => void; + setLoans: React.Dispatch>; +} + +export default function EditLoan({loan, setSideView, setLoans}: EditLoanProps) { + + const [initialFormData, setInitialFormData] = useState({borrower: loan.borrower.name, startDate: loan.startDate, endDate: loan.endDate, borrowerPhone: loan.borrower.phone, borrowerMail: loan.borrower.email}) + const [formData, setFormData] = useState(initialFormData); + + const phoneRequired = formData.borrowerMail?.trim() === ""; + const emailRequired = formData.borrowerPhone?.trim() === ""; + + const [borrowers, setBorrowers] = useState([]); + useEffect(() => { + fetch("/api/borrower") + .then(res => res.json()) + .then(data => setBorrowers(data)) + }, []); + + + async function handleSubmit(e: React.FormEvent) { + e.preventDefault(); + + if ( + !formData.borrower.trim() + || !formData.startDate + || !formData.endDate + || (!formData.borrowerPhone?.trim() && !formData.borrowerMail?.trim()) + ) { + alert("Please fill in all required fields"); + return; + } + + const updatedLoan = await updateLoan( + loan.id, + formData.startDate, + formData.endDate, + formData.borrower, + loan.borrower.id, + formData.borrowerPhone, + formData.borrowerMail + ); + if (!updatedLoan) return; + + setLoans(prev => + prev.map(loan => loan.id === updatedLoan.id ? updatedLoan : loan) + + ) + + setInitialFormData(formData); + + + + } + + return( + <> + {/* Dark backdrop */} +
setSideView("")} + /> + + {/* Right-side panel */} +
+ + {/* Vertical split */} +
+ {/* Left side of a panel */} +
+

Edit loan

+
+ + + +
+ +
+ + + { + const borrower = borrowers.find(borrower => borrower.name === e.target.value); + // TODO: Better to use functional updates because React hasn't re-rendered the component yet and should not do two state changes at once due to batching + setFormData(prev => ({ + ...prev, + borrower: e.target.value, + borrowerPhone: borrower?.phone || "", + borrowerMail: borrower?.email || "" + })) + }} + /> + + {borrowers.map(borrower => )} + + + { + const selected = e.target.value; + setFormData({...formData, startDate: new Date(selected)}) + }} + /> + + { + const selected = e.target.value; + if (selected < formData.startDate.toLocaleDateString()) return; + setFormData({...formData, endDate: new Date(selected)}); + + }} + /> + + { + console.log(e.target.value); + setFormData({...formData, borrowerPhone: e.target.value}) + console.log(formData); + }} + /> + + { + setFormData({...formData, borrowerMail: e.target.value}) + }} + /> + +
+ --------------------------------------------------------------------------------------- +

Items

+
+
+ {/*equipmentData.items.map((unit, index) => ( + hasActiveLoan = unit.activeLoan != null, +
+

Unit {index + 1}

+ { unit.activeLoan && (unit.activeLoan.status !== "Returned") &&

Borrowed

} + { (unit.activeLoan == null || unit.activeLoan.status === "Returned") && } + +
+ )) */} +
+
+
+ + {/* Right side of panel */} +
+
+ +
+
+ {/*
+

{equipmentData.name}

+

{equipmentData.category.name}

+

{equipmentData.items.length - loanCount(equipmentData)}/{equipmentData.items.length} Available

+
*/} + ---------------------------------------------------------------------------------- +

History

+
+
+
+
+ + ) +} \ No newline at end of file diff --git a/labman/src/lib/actions.ts b/labman/src/lib/actions.ts index 005802b..ec17ef7 100644 --- a/labman/src/lib/actions.ts +++ b/labman/src/lib/actions.ts @@ -3,6 +3,7 @@ import prisma from "@/lib/prisma" import {revalidatePath} from "next/cache"; import {deleteSession, validateSessionToken} from "@/auth/session" import {cookies} from "next/headers"; +import {Borrower} from "@/generated/prisma"; export async function deleteUser(userId : number) { @@ -126,27 +127,41 @@ export async function updateEquipment (equipmentId: number, name: string, catego return equipment; } -export async function addLoan (borrowerName : string, start : string, end : string, unitId : number, phone : string | null, email : string | null) { - const dateStart = new Date(start); - const dateEnd = new Date(end); +export async function addBorrower(name: string, phone?: string | null, email?: string | null, borrowerId?: number) : Promise { const user = await getUser(); + if (!user) {alert("Could not find a valid user"); return null} + let borrower : Borrower | null = null; - if (!user) {alert("Could not find a valid user"); return} - - let borrower; // If phone or email is actually empty, set it to null if (phone?.trim() === "") {phone = null} - if (email?.trim() === "") {email = null} + // If borrowerId is provided, update the borrower with the provided information + if (borrowerId) { + borrower = await prisma.borrower.findUnique({where:{id: borrowerId}}) + /* TODO: Potentially unsafe. The function can in theory be called with an unrelated id updating the wrong borrower + Since the unique values phone and mail can change they can't be used to verify the borrower. + A possible solution is to compare the old values with what is currently stored in the database. + But it shouldn't really be a big deal as there is now way for the client to abuse it.*/ + if (!borrower) {alert("Could not find borrower with id " + borrowerId); return null} + + borrower = await prisma.borrower.update( + { + where: {id: borrowerId}, + data: {name: name, phone: phone, email: email} + } + ) + return borrower; + } + if (phone) { borrower = await prisma.borrower.findUnique({where:{phone: phone}}) - /* if (borrower && borrower.name !== borrowerName) { - if (window.confirm(`A borrower with the same phone number already exists with a different name (${borrower.name}). Click OK to assign this loan to ${borrowerName}. Click Cancel to assign it to ${borrower.name} instead.`)) { - borrower = null; - } - } */ + /* if (borrower && borrower.name !== borrowerName) { + if (window.confirm(`A borrower with the same phone number already exists with a different name (${borrower.name}). Click OK to assign this loan to ${borrowerName}. Click Cancel to assign it to ${borrower.name} instead.`)) { + borrower = null; + } + } */ } else if (email) { borrower = await prisma.borrower.findUnique({where:{email: email}}) @@ -156,16 +171,13 @@ export async function addLoan (borrowerName : string, start : string, end : stri } } */ } else { - alert("No borrower phone/email provided"); return; + alert("No borrower phone/email provided"); return null; } - - - if (!borrower) { borrower = await prisma.borrower.create({ data: { - name: borrowerName, + name: name, phone: phone, email: email, note: "", @@ -173,6 +185,42 @@ export async function addLoan (borrowerName : string, start : string, end : stri } }) } + return borrower; +} + +export async function updateLoan (loanId: number, start : Date, end : Date, borrowerName : string, borrowerId : number, phone? : string | null, email? : string | null) { + if (await getUser() === null) {alert("Could not find a valid user"); return} + + const borrower = await addBorrower(borrowerName, phone, email, borrowerId) + if (!borrower) {return} + + const loan = await prisma.loan.update({ + where: { + id: loanId + }, + data: { + startDate: start, + endDate: end, + borrowerId: borrower.id + }, + include: { + borrower: true, + item: { include: { equipment: true }} + } + }) + revalidatePath("/loans"); + return (loan) + +} + +export async function addLoan (borrowerName : string, start : string, end : string, unitId : number, phone : string | null, email : string | null) { + const dateStart = new Date(start); + const dateEnd = new Date(end); + const user = await getUser(); + if (!user) {alert("Could not find a valid user"); return} + + const borrower = await addBorrower(borrowerName, phone, email) + if (!borrower) {return} const loan = await prisma.loan.create({ data: { @@ -209,7 +257,9 @@ export async function getUser() { } }) return tSession?.user; - } + } else { + return null; + } } From 469cfd2327ecdf32677f566026e777dd606098a1 Mon Sep 17 00:00:00 2001 From: Ola Bekkevold Date: Wed, 25 Feb 2026 09:17:08 +0100 Subject: [PATCH 02/14] feat: improve input and error handling make user inputs safer to correct the user before they can submit and add proper error handling for possible contradictions when updating a loan or borrower Refs: #46 #30 #38 --- labman/src/components/loans/EditLoan.tsx | 76 +++++++++++------------- labman/src/lib/actions.ts | 46 +++++++++----- labman/src/types/Loan.ts | 28 +++++++++ 3 files changed, 94 insertions(+), 56 deletions(-) diff --git a/labman/src/components/loans/EditLoan.tsx b/labman/src/components/loans/EditLoan.tsx index 97ddbf6..e14a3ba 100644 --- a/labman/src/components/loans/EditLoan.tsx +++ b/labman/src/components/loans/EditLoan.tsx @@ -3,34 +3,8 @@ import {loanCount} from "@/utils/inventoryUtils"; import {useEffect, useState} from "react"; import {updateLoan} from "@/lib/actions"; +import {Loan} from "@/types/Loan"; -type Loan = { - id: number; - startDate: Date; - endDate: Date; - status: string; - - borrower: { - id: number; - name: string; - phone?: string | null; - email?: string | null - note?: string | null - creationDate: Date; - - } - item: { - id: number; - equipment: { - id: number; - name: string; - categoryId: number; - image: string | null; - createdAt: Date; - - } - } -} type Borrower = { id: number; @@ -58,7 +32,7 @@ export default function EditLoan({loan, setSideView, setLoans}: EditLoanProps) { fetch("/api/borrower") .then(res => res.json()) .then(data => setBorrowers(data)) - }, []); + }, [initialFormData]); async function handleSubmit(e: React.FormEvent) { @@ -73,17 +47,24 @@ export default function EditLoan({loan, setSideView, setLoans}: EditLoanProps) { alert("Please fill in all required fields"); return; } - - const updatedLoan = await updateLoan( - loan.id, - formData.startDate, - formData.endDate, - formData.borrower, - loan.borrower.id, - formData.borrowerPhone, - formData.borrowerMail - ); - if (!updatedLoan) return; + let updatedLoan: Loan; + const res = await updateLoan( + loan.id, + formData.startDate, + formData.endDate, + formData.borrower, + loan.borrower.id, + formData.borrowerPhone, + formData.borrowerMail + ); + console.log(res); + + if (res.type === "error") { + alert(res.message); + return; + } else if (res.type === "success") { + updatedLoan = res.data; + } setLoans(prev => prev.map(loan => loan.id === updatedLoan.id ? updatedLoan : loan) @@ -176,9 +157,15 @@ export default function EditLoan({loan, setSideView, setLoans}: EditLoanProps) { pattern="[0-9]{8}" className="side-form-input" onChange={(e) => { - console.log(e.target.value); + e.target.setCustomValidity(""); + if (e.target.value.length === 8) { + if (borrowers.find(borrower => borrower.phone === e.target.value && borrower.id !== loan.borrower.id)) { + e.target.setCustomValidity("A borrower with the same phone number already exists"); + e.target.reportValidity(); + } + } setFormData({...formData, borrowerPhone: e.target.value}) - console.log(formData); + }} /> @@ -188,6 +175,13 @@ export default function EditLoan({loan, setSideView, setLoans}: EditLoanProps) { type="email" className="side-form-input" onChange={(e) => { + e.target.setCustomValidity(""); + if (e.target.checkValidity()) { + if (borrowers.find(borrower => borrower.email === e.target.value && borrower.id !== loan.borrower.id)) { + e.target.setCustomValidity("A borrower with the same email already exists"); + e.target.reportValidity(); + } + } setFormData({...formData, borrowerMail: e.target.value}) }} /> diff --git a/labman/src/lib/actions.ts b/labman/src/lib/actions.ts index ec17ef7..a147ae1 100644 --- a/labman/src/lib/actions.ts +++ b/labman/src/lib/actions.ts @@ -4,6 +4,9 @@ import {revalidatePath} from "next/cache"; import {deleteSession, validateSessionToken} from "@/auth/session" import {cookies} from "next/headers"; import {Borrower} from "@/generated/prisma"; +import {Loan} from "@/types/Loan"; + +type ActionResult = | { type: "success"; data: T} | { type: "confirm"; message: string} | { type: "error"; message: string} export async function deleteUser(userId : number) { @@ -127,9 +130,9 @@ export async function updateEquipment (equipmentId: number, name: string, catego return equipment; } -export async function addBorrower(name: string, phone?: string | null, email?: string | null, borrowerId?: number) : Promise { +export async function addBorrower(name: string, phone?: string | null, email?: string | null, borrowerId?: number) : Promise> { const user = await getUser(); - if (!user) {alert("Could not find a valid user"); return null} + if (!user) {return {type: "error", message: "Could not find a valid user"}} let borrower : Borrower | null = null; @@ -144,7 +147,20 @@ export async function addBorrower(name: string, phone?: string | null, email?: s Since the unique values phone and mail can change they can't be used to verify the borrower. A possible solution is to compare the old values with what is currently stored in the database. But it shouldn't really be a big deal as there is now way for the client to abuse it.*/ - if (!borrower) {alert("Could not find borrower with id " + borrowerId); return null} + if (!borrower) {return {type: "error", message: "Could not find borrower with id " + borrowerId}} + + + if (phone && borrower.phone !== phone) { + console.log("Phone number has changed, checking for duplicates") + if (await prisma.borrower.findUnique({where:{phone: phone}})) { + console.log("Borrower with phone number already exists") + + return {type: "error", message: `A borrower with the same phone number already exists.`} + } + } else if (email && borrower.email !== email) { + if (await prisma.borrower.findUnique({where:{email: email}})) + return {type: "error", message: `A borrower with the same email already exists.`} + } borrower = await prisma.borrower.update( { @@ -152,7 +168,7 @@ export async function addBorrower(name: string, phone?: string | null, email?: s data: {name: name, phone: phone, email: email} } ) - return borrower; + return {type: "success", data: borrower}; } if (phone) { @@ -171,7 +187,7 @@ export async function addBorrower(name: string, phone?: string | null, email?: s } } */ } else { - alert("No borrower phone/email provided"); return null; + return {type: "error", message: "No borrower phone/email provided"} } if (!borrower) { @@ -185,14 +201,14 @@ export async function addBorrower(name: string, phone?: string | null, email?: s } }) } - return borrower; + return {type: "success", data: borrower}; } -export async function updateLoan (loanId: number, start : Date, end : Date, borrowerName : string, borrowerId : number, phone? : string | null, email? : string | null) { - if (await getUser() === null) {alert("Could not find a valid user"); return} +export async function updateLoan (loanId: number, start : Date, end : Date, borrowerName : string, borrowerId : number, phone? : string | null, email? : string | null) : Promise> { + if (await getUser() === null) {return {type:"error", message: "Could not find a valid user"}} - const borrower = await addBorrower(borrowerName, phone, email, borrowerId) - if (!borrower) {return} + const res = await addBorrower(borrowerName, phone, email, borrowerId) + if (res.type !== "success") {return {type: "error", message: res.message}} const loan = await prisma.loan.update({ where: { @@ -201,7 +217,7 @@ export async function updateLoan (loanId: number, start : Date, end : Date, borr data: { startDate: start, endDate: end, - borrowerId: borrower.id + borrowerId: res.data.id }, include: { borrower: true, @@ -209,7 +225,7 @@ export async function updateLoan (loanId: number, start : Date, end : Date, borr } }) revalidatePath("/loans"); - return (loan) + return {type: "success", data: loan}; } @@ -219,15 +235,15 @@ export async function addLoan (borrowerName : string, start : string, end : stri const user = await getUser(); if (!user) {alert("Could not find a valid user"); return} - const borrower = await addBorrower(borrowerName, phone, email) - if (!borrower) {return} + const res = await addBorrower(borrowerName, phone, email) + if (res.type !== "success") {return} const loan = await prisma.loan.create({ data: { startDate: dateStart, endDate: dateEnd, status: "Active", - borrowerId: borrower.id, + borrowerId: res.data.id, userId: user.id, itemId: unitId } diff --git a/labman/src/types/Loan.ts b/labman/src/types/Loan.ts index db8082e..c8ef8b1 100644 --- a/labman/src/types/Loan.ts +++ b/labman/src/types/Loan.ts @@ -3,6 +3,34 @@ returnLoan: (id: number) => void; } +export type Loan = { + id: number; + startDate: Date; + endDate: Date; + status: string; + + borrower: { + id: number; + name: string; + phone?: string | null; + email?: string | null + note?: string | null + creationDate: Date; + + } + item: { + id: number; + equipment: { + id: number; + name: string; + categoryId: number; + image: string | null; + createdAt: Date; + + } + } +} + type Borrower = { id: number; name: string; From 5ab9c6a709dbb581efadde8974ffc53c43ae0bbb Mon Sep 17 00:00:00 2001 From: Ola Bekkevold Date: Thu, 26 Feb 2026 16:41:16 +0100 Subject: [PATCH 03/14] feat: global side view context and early refactoring of sideview Refs: #52 --- labman/src/app/(main)/layout.tsx | 27 +-- labman/src/app/(main)/popupProvider.tsx | 22 -- labman/src/app/sideViewContext.tsx | 26 +++ labman/src/components/core/CardList.tsx | 5 +- labman/src/components/core/NavBar.tsx | 1 - labman/src/components/core/SideView.tsx | 108 ++++++++++ labman/src/components/loans/EditLoan.tsx | 243 +++++++++-------------- 7 files changed, 241 insertions(+), 191 deletions(-) delete mode 100644 labman/src/app/(main)/popupProvider.tsx create mode 100644 labman/src/app/sideViewContext.tsx create mode 100644 labman/src/components/core/SideView.tsx diff --git a/labman/src/app/(main)/layout.tsx b/labman/src/app/(main)/layout.tsx index 1fe817c..53b28b1 100644 --- a/labman/src/app/(main)/layout.tsx +++ b/labman/src/app/(main)/layout.tsx @@ -5,6 +5,7 @@ import NavBar from "@/components/core/NavBar"; //import {PopupProvider} from "./popupProvider" import {getUser} from "@/lib/actions"; import SideBar from "@/components/core/SideBar"; +import {SideViewProvider} from "@/app/sideViewContext"; const spartan = League_Spartan({ subsets: ["latin"], @@ -27,20 +28,20 @@ export default async function RootLayout({ return ( - - -
- - - -
- - {children} -
-
+ +
+ + + +
+ + {children} +
+
+
); diff --git a/labman/src/app/(main)/popupProvider.tsx b/labman/src/app/(main)/popupProvider.tsx deleted file mode 100644 index 7d919a1..0000000 --- a/labman/src/app/(main)/popupProvider.tsx +++ /dev/null @@ -1,22 +0,0 @@ -/*"use client" -import {createContext, useState} from "react"; - -export const popupContext = createContext(null); - -export const PopupProvider = ({children}) => { - const [isOpen, setIsOpen] = useState(false); - const [message, setMessage] = useState(""); - - return ( - - <> - {children} -
-
-

{message}

-
-
- -
- ) -} */ \ No newline at end of file diff --git a/labman/src/app/sideViewContext.tsx b/labman/src/app/sideViewContext.tsx new file mode 100644 index 0000000..a304d85 --- /dev/null +++ b/labman/src/app/sideViewContext.tsx @@ -0,0 +1,26 @@ +"use client"; + +import React, { createContext, useContext, useState } from "react"; + +type SideViewCtx = { + sideView: string; + setSideView: React.Dispatch>; +}; + +const SideViewContext = createContext(null); + +export function SideViewProvider({ children, initialType = "",}: { children: React.ReactNode; initialType?: string; }) { + const [sideView, setSideView] = useState(initialType); + + return ( + + {children} + + ); +} + +export function useSideView() { + const ctx = useContext(SideViewContext); + if (!ctx) throw new Error("useString must be used within "); + return ctx; +} \ No newline at end of file diff --git a/labman/src/components/core/CardList.tsx b/labman/src/components/core/CardList.tsx index 50cd954..62675bd 100644 --- a/labman/src/components/core/CardList.tsx +++ b/labman/src/components/core/CardList.tsx @@ -6,6 +6,7 @@ import {UserClass} from "@/types/User"; import {returnLoan, deleteLoan, deleteUser} from "@/lib/actions"; import {LoanClass} from "@/types/Loan"; import EditLoan from "@/components/loans/EditLoan"; +import {useSideView} from "@/app/sideViewContext"; type Loans = { @@ -52,9 +53,10 @@ export default function CardList({ loansProp = [], usersProp = []}: CardListProp const [username, setUsername] = useState(""); const [password, setPassword] = useState(""); - const [sideView, setSideView] = useState(""); const [selectedLoanId, setSelectedLoanId] = useState(null); + const { sideView, setSideView } = useSideView(); + async function handleSubmit(e: React.FormEvent) { e.preventDefault(); @@ -125,7 +127,6 @@ export default function CardList({ loansProp = [], usersProp = []}: CardListProp
{ sideView == "loanEdit" && selectedLoanId && loan.id === selectedLoanId)!} - setSideView={setSideView} setLoans={setLoans} />} diff --git a/labman/src/components/core/NavBar.tsx b/labman/src/components/core/NavBar.tsx index c0f2ae7..127d9bf 100644 --- a/labman/src/components/core/NavBar.tsx +++ b/labman/src/components/core/NavBar.tsx @@ -1,5 +1,4 @@ "use client" -import Button from "@/components/core/Button"; import PathName from "@/components/core/PathName"; import {logout} from "@/lib/actions"; diff --git a/labman/src/components/core/SideView.tsx b/labman/src/components/core/SideView.tsx new file mode 100644 index 0000000..234842d --- /dev/null +++ b/labman/src/components/core/SideView.tsx @@ -0,0 +1,108 @@ +import {JSX} from "react"; +import {useSideView} from "@/app/sideViewContext"; + + +export default function SideView( {children}: {children: JSX.Element}) { + const {sideView, setSideView} = useSideView(); + return ( + <> + {/* Dark backdrop */} +
setSideView("")} + /> + + {/* Right-side panel */} +
+ + {/* Vertical split */} +
+ {/* Left side of a panel */} +
+

Title

+
+ {/* + + + */} +
+ +
+ {children} + {/* +
+ + + + + + + + + + +
*/} +
+ --------------------------------------------------------------------------------------- +

Items

+ {/*
+
+ {equipmentData.items.map((unit, index) => ( + hasActiveLoan = unit.activeLoan != null, +
+

Unit {index + 1}

+ { unit.activeLoan && (unit.activeLoan.status !== "Returned") &&

Borrowed

} + { (unit.activeLoan == null || unit.activeLoan.status === "Returned") && } + +
+ )) } +
+
*/} +
+ + {/* Right side of panel */} +
+
+ +
+
+ {/*
+

{equipmentData.name}

+

{equipmentData.category.name}

+

{equipmentData.items.length - loanCount(equipmentData)}/{equipmentData.items.length} Available

+
*/} + ---------------------------------------------------------------------------------- +

History

+
+
+
+
+ + ) +} \ No newline at end of file diff --git a/labman/src/components/loans/EditLoan.tsx b/labman/src/components/loans/EditLoan.tsx index e14a3ba..c7e9c5b 100644 --- a/labman/src/components/loans/EditLoan.tsx +++ b/labman/src/components/loans/EditLoan.tsx @@ -4,6 +4,7 @@ import {loanCount} from "@/utils/inventoryUtils"; import {useEffect, useState} from "react"; import {updateLoan} from "@/lib/actions"; import {Loan} from "@/types/Loan"; +import SideView from "@/components/core/SideView"; type Borrower = { @@ -15,11 +16,10 @@ type Borrower = { interface EditLoanProps { loan: Loan; - setSideView: (view: string) => void; setLoans: React.Dispatch>; } -export default function EditLoan({loan, setSideView, setLoans}: EditLoanProps) { +export default function EditLoan({loan, setLoans}: EditLoanProps) { const [initialFormData, setInitialFormData] = useState({borrower: loan.borrower.name, startDate: loan.startDate, endDate: loan.endDate, borrowerPhone: loan.borrower.phone, borrowerMail: loan.borrower.email}) const [formData, setFormData] = useState(initialFormData); @@ -34,7 +34,6 @@ export default function EditLoan({loan, setSideView, setLoans}: EditLoanProps) { .then(data => setBorrowers(data)) }, [initialFormData]); - async function handleSubmit(e: React.FormEvent) { e.preventDefault(); @@ -70,161 +69,99 @@ export default function EditLoan({loan, setSideView, setLoans}: EditLoanProps) { prev.map(loan => loan.id === updatedLoan.id ? updatedLoan : loan) ) - setInitialFormData(formData); - - - } return( <> - {/* Dark backdrop */} -
setSideView("")} - /> - - {/* Right-side panel */} -
- - {/* Vertical split */} -
- {/* Left side of a panel */} -
-

Edit loan

-
- - - -
- -
-
- - { - const borrower = borrowers.find(borrower => borrower.name === e.target.value); - // TODO: Better to use functional updates because React hasn't re-rendered the component yet and should not do two state changes at once due to batching - setFormData(prev => ({ - ...prev, - borrower: e.target.value, - borrowerPhone: borrower?.phone || "", - borrowerMail: borrower?.email || "" - })) - }} - /> - - {borrowers.map(borrower => )} - - - { - const selected = e.target.value; - setFormData({...formData, startDate: new Date(selected)}) - }} - /> - - { - const selected = e.target.value; - if (selected < formData.startDate.toLocaleDateString()) return; - setFormData({...formData, endDate: new Date(selected)}); - - }} - /> - - { - e.target.setCustomValidity(""); - if (e.target.value.length === 8) { - if (borrowers.find(borrower => borrower.phone === e.target.value && borrower.id !== loan.borrower.id)) { - e.target.setCustomValidity("A borrower with the same phone number already exists"); - e.target.reportValidity(); - } - } - setFormData({...formData, borrowerPhone: e.target.value}) - - }} - /> - - { - e.target.setCustomValidity(""); - if (e.target.checkValidity()) { - if (borrowers.find(borrower => borrower.email === e.target.value && borrower.id !== loan.borrower.id)) { - e.target.setCustomValidity("A borrower with the same email already exists"); - e.target.reportValidity(); - } - } - setFormData({...formData, borrowerMail: e.target.value}) - }} - /> -
-
- --------------------------------------------------------------------------------------- -

Items

-
-
- {/*equipmentData.items.map((unit, index) => ( - hasActiveLoan = unit.activeLoan != null, -
-

Unit {index + 1}

- { unit.activeLoan && (unit.activeLoan.status !== "Returned") &&

Borrowed

} - { (unit.activeLoan == null || unit.activeLoan.status === "Returned") && } - -
- )) */} -
-
-
- - {/* Right side of panel */} -
-
- -
-
- {/*
-

{equipmentData.name}

-

{equipmentData.category.name}

-

{equipmentData.items.length - loanCount(equipmentData)}/{equipmentData.items.length} Available

-
*/} - ---------------------------------------------------------------------------------- -

History

-
-
-
-
+ +
+ + { + const borrower = borrowers.find(borrower => borrower.name === e.target.value); + // TODO: Better to use functional updates because React hasn't re-rendered the component yet and should not do two state changes at once due to batching + setFormData(prev => ({ + ...prev, + borrower: e.target.value, + borrowerPhone: borrower?.phone || "", + borrowerMail: borrower?.email || "" + })) + }} + /> + + {borrowers.map(borrower => )} + + + { + const selected = e.target.value; + setFormData({...formData, startDate: new Date(selected)}) + }} + /> + + { + const selected = e.target.value; + if (selected < formData.startDate.toLocaleDateString()) return; + setFormData({...formData, endDate: new Date(selected)}); + + }} + /> + + { + e.target.setCustomValidity(""); + if (e.target.value.length === 8) { + if (borrowers.find(borrower => borrower.phone === e.target.value && borrower.id !== loan.borrower.id)) { + e.target.setCustomValidity("A borrower with the same phone number already exists"); + e.target.reportValidity(); + } + } + setFormData({...formData, borrowerPhone: e.target.value}) + + }} + /> + + { + e.target.setCustomValidity(""); + if (e.target.checkValidity()) { + if (borrowers.find(borrower => borrower.email === e.target.value && borrower.id !== loan.borrower.id)) { + e.target.setCustomValidity("A borrower with the same email already exists"); + e.target.reportValidity(); + } + } + setFormData({...formData, borrowerMail: e.target.value}) + }} + /> +
+
) } \ No newline at end of file From e1246ed22b8aa019818160f7a4d7518045ac65fd Mon Sep 17 00:00:00 2001 From: OlaBekkevold Date: Thu, 26 Feb 2026 22:36:18 +0100 Subject: [PATCH 04/14] feat: mostly complete refactor of loan edit Refs: #52 --- .../core/{ => SideView}/SideView.tsx | 4 +- labman/src/components/loans/EditLoan.tsx | 187 +++++++++--------- 2 files changed, 101 insertions(+), 90 deletions(-) rename labman/src/components/core/{ => SideView}/SideView.tsx (97%) diff --git a/labman/src/components/core/SideView.tsx b/labman/src/components/core/SideView/SideView.tsx similarity index 97% rename from labman/src/components/core/SideView.tsx rename to labman/src/components/core/SideView/SideView.tsx index 234842d..33c77cb 100644 --- a/labman/src/components/core/SideView.tsx +++ b/labman/src/components/core/SideView/SideView.tsx @@ -19,7 +19,7 @@ export default function SideView( {children}: {children: JSX.Element}) {
{/* Left side of a panel */}
-

Title

+

{sideView === "loanEdit" ? "Edit loan" : "Unknown"}

{/* @@ -89,7 +89,7 @@ export default function SideView( {children}: {children: JSX.Element}) { {/* Right side of panel */}
- +
{/*
diff --git a/labman/src/components/loans/EditLoan.tsx b/labman/src/components/loans/EditLoan.tsx index c7e9c5b..f6a42ce 100644 --- a/labman/src/components/loans/EditLoan.tsx +++ b/labman/src/components/loans/EditLoan.tsx @@ -1,10 +1,9 @@ "use client" -import {loanCount} from "@/utils/inventoryUtils"; import {useEffect, useState} from "react"; import {updateLoan} from "@/lib/actions"; import {Loan} from "@/types/Loan"; -import SideView from "@/components/core/SideView"; +import SideView from "@/components/core/SideView/SideView"; type Borrower = { @@ -75,92 +74,104 @@ export default function EditLoan({loan, setLoans}: EditLoanProps) { return( <> -
- - { - const borrower = borrowers.find(borrower => borrower.name === e.target.value); - // TODO: Better to use functional updates because React hasn't re-rendered the component yet and should not do two state changes at once due to batching - setFormData(prev => ({ - ...prev, - borrower: e.target.value, - borrowerPhone: borrower?.phone || "", - borrowerMail: borrower?.email || "" - })) - }} - /> - - {borrowers.map(borrower => )} - - - { - const selected = e.target.value; - setFormData({...formData, startDate: new Date(selected)}) - }} - /> - - { - const selected = e.target.value; - if (selected < formData.startDate.toLocaleDateString()) return; - setFormData({...formData, endDate: new Date(selected)}); - - }} - /> - - { - e.target.setCustomValidity(""); - if (e.target.value.length === 8) { - if (borrowers.find(borrower => borrower.phone === e.target.value && borrower.id !== loan.borrower.id)) { - e.target.setCustomValidity("A borrower with the same phone number already exists"); - e.target.reportValidity(); - } - } - setFormData({...formData, borrowerPhone: e.target.value}) - - }} - /> - - { - e.target.setCustomValidity(""); - if (e.target.checkValidity()) { - if (borrowers.find(borrower => borrower.email === e.target.value && borrower.id !== loan.borrower.id)) { - e.target.setCustomValidity("A borrower with the same email already exists"); - e.target.reportValidity(); - } - } - setFormData({...formData, borrowerMail: e.target.value}) - }} - /> -
+ <> +
+ + + +
+ +
+ +
+ + { + const borrower = borrowers.find(borrower => borrower.name === e.target.value); + // TODO: Better to use functional updates because React hasn't re-rendered the component yet and should not do two state changes at once due to batching + setFormData(prev => ({ + ...prev, + borrower: e.target.value, + borrowerPhone: borrower?.phone || "", + borrowerMail: borrower?.email || "" + })) + }} + /> + + {borrowers.map(borrower => )} + + + { + const selected = e.target.value; + setFormData({...formData, startDate: new Date(selected)}) + }} + /> + + { + const selected = e.target.value; + if (selected < formData.startDate.toLocaleDateString()) return; + setFormData({...formData, endDate: new Date(selected)}); + + }} + /> + + { + e.target.setCustomValidity(""); + if (e.target.value.length === 8) { + if (borrowers.find(borrower => borrower.phone === e.target.value && borrower.id !== loan.borrower.id)) { + e.target.setCustomValidity("A borrower with the same phone number already exists"); + e.target.reportValidity(); + } + } + setFormData({...formData, borrowerPhone: e.target.value}) + + }} + /> + + { + e.target.setCustomValidity(""); + if (e.target.checkValidity()) { + if (borrowers.find(borrower => borrower.email === e.target.value && borrower.id !== loan.borrower.id)) { + e.target.setCustomValidity("A borrower with the same email already exists"); + e.target.reportValidity(); + } + } + setFormData({...formData, borrowerMail: e.target.value}) + }} + /> +
+
+ +
) From b9f7cce649ca5a0a6fe2bb85b2c81c9b11169b80 Mon Sep 17 00:00:00 2001 From: Ola Bekkevold Date: Fri, 27 Feb 2026 15:30:40 +0100 Subject: [PATCH 05/14] feat: enhance side view components and improve data handling for loans and equipment Refs: #52 --- labman/src/app/(main)/layout.tsx | 2 +- labman/src/app/(main)/loans/page.tsx | 17 +- labman/src/components/core/CardList.tsx | 7 +- .../src/components/core/SideView/ItemList.tsx | 29 +++ .../src/components/core/SideView/SideView.tsx | 16 +- .../components/inventory/EquipmentClient.tsx | 5 +- .../components/inventory/EquipmentInfo.tsx | 104 +++----- labman/src/components/inventory/LoanView.tsx | 225 +++++++----------- labman/src/components/loans/EditLoan.tsx | 2 +- labman/src/lib/actions.ts | 17 +- labman/src/types/Loan.ts | 22 +- 11 files changed, 199 insertions(+), 247 deletions(-) create mode 100644 labman/src/components/core/SideView/ItemList.tsx diff --git a/labman/src/app/(main)/layout.tsx b/labman/src/app/(main)/layout.tsx index 53b28b1..c3fd74b 100644 --- a/labman/src/app/(main)/layout.tsx +++ b/labman/src/app/(main)/layout.tsx @@ -29,7 +29,7 @@ export default async function RootLayout({ return ( - +