266 lines
10 KiB
JavaScript
266 lines
10 KiB
JavaScript
import React, { useState } from 'react';
|
|
import { Button } from './ui/button.js';
|
|
|
|
/**
|
|
* LoanRepayment
|
|
* ────────────────────────────────────────────────────────────────
|
|
* If `schools.length === 0` we first show a quick-fill estimator
|
|
* that lets a free-tier user type a school name, choose “degree type”,
|
|
* and enter an annual-tuition guess. We then create a minimal
|
|
* school object and push it into `schools`, after which the full
|
|
* repayment form (your original code) appears.
|
|
*/
|
|
function LoanRepayment({
|
|
/* ORIGINAL PROPS */
|
|
schools = [],
|
|
salaryData,
|
|
setResults,
|
|
setLoading,
|
|
setPersistedROI,
|
|
programLength,
|
|
|
|
/* NEW: parent must hand us a setter so we can inject the stub */
|
|
setSchools,
|
|
}) {
|
|
/* ------------------------- quick-fill state ------------------------- */
|
|
const [scratch, setScratch] = useState({
|
|
school: '',
|
|
programType: '',
|
|
tuition: '',
|
|
});
|
|
|
|
/* ------------------------- calculator state ------------------------ */
|
|
const [expectedSalary, setExpectedSalary] = useState(0);
|
|
const [tuitionType, setTuitionType] = useState('inState');
|
|
const [interestRate, setInterestRate] = useState(5.5);
|
|
const [loanTerm, setLoanTerm] = useState(10);
|
|
const [extraPayment, setExtraPayment] = useState(0);
|
|
const [currentSalary, setCurrentSalary] = useState(0);
|
|
const [error, setError] = useState(null);
|
|
|
|
/* ------------------------- validation ------------------------------ */
|
|
const validateInputs = () => {
|
|
if (!schools?.length) { setError('Missing school data.'); return false; }
|
|
if (isNaN(interestRate)||interestRate<=0) { setError('Interest rate > 0'); return false; }
|
|
if (isNaN(loanTerm)||loanTerm<=0) { setError('Loan term > 0'); return false; }
|
|
if (isNaN(extraPayment)||extraPayment<0) { setError('Extra pmt ≥ 0'); return false; }
|
|
if (isNaN(currentSalary)||currentSalary<0){ setError('Current salary ≥0');return false; }
|
|
if (isNaN(expectedSalary)||expectedSalary<0){setError('Expected salary ≥0');return false;}
|
|
setError(null);
|
|
return true;
|
|
};
|
|
|
|
/* ------------------------- main calculation ----------------------- */
|
|
const calculateLoanDetails = () => {
|
|
if (!validateInputs()) return;
|
|
setLoading?.(true);
|
|
|
|
const pickTuition = (school, resid, grad) => {
|
|
const tryNum = v => isNaN(v) ? 0 : Number(v);
|
|
|
|
if (grad) {
|
|
return resid === 'inState'
|
|
? tryNum(school.inStateGraduate) || tryNum(school.inState) || tryNum(school.tuition)
|
|
: tryNum(school.outStateGraduate) || tryNum(school.outOfState)|| tryNum(school.tuition);
|
|
}
|
|
/* under-grad */
|
|
return resid === 'inState'
|
|
? tryNum(school.inState) || tryNum(school.inStateGraduate) || tryNum(school.tuition)
|
|
: tryNum(school.outOfState) || tryNum(school.outStateGraduate) || tryNum(school.tuition);
|
|
};
|
|
|
|
const results = schools.map((school) => {
|
|
/* your existing repayment logic — unchanged */
|
|
const programLen = Number(school.programLength);
|
|
|
|
let ugYears=0, gradYears=0;
|
|
if (school.degreeType.includes('Associate')) ugYears=2;
|
|
else if (school.degreeType.includes('Bachelor')) ugYears=4;
|
|
else if (school.degreeType.includes('Master')) { ugYears=4; gradYears=2; }
|
|
else if (school.degreeType.includes('First Professional')||school.degreeType.includes('Doctoral'))
|
|
{ ugYears=4; gradYears=4; }
|
|
else if (school.degreeType.includes('Certificate')) ugYears=1;
|
|
else { ugYears=Math.min(programLen,4); gradYears=Math.max(programLen-4,0); }
|
|
|
|
const ugTuit = pickTuition(school, tuitionType, /*grad?*/ false);
|
|
const gradTuit = pickTuition(school, tuitionType, /*grad?*/ true);
|
|
|
|
let totalTuition = ugYears * ugTuit +
|
|
gradYears* gradTuit;
|
|
|
|
const r = Number(interestRate)/12/100;
|
|
const n = Number(loanTerm)*12;
|
|
const pmtMin = totalTuition * (r*Math.pow(1+r,n))/(Math.pow(1+r,n)-1);
|
|
const pmt = Number(pmtMin)+Number(extraPayment);
|
|
|
|
let bal=totalTuition, months=0;
|
|
while (bal>0 && months<n*2){
|
|
months++;
|
|
const interest = bal*r;
|
|
bal -= Math.max(pmt - interest,0);
|
|
}
|
|
const totalCost = pmt*months;
|
|
|
|
/* safe net-gain estimate */
|
|
const salary = Number(expectedSalary)||0;
|
|
const cur = Number(currentSalary)||0;
|
|
const netGain = salary
|
|
? (salary*loanTerm - totalCost - cur*loanTerm*Math.pow(1.03,loanTerm)).toFixed(2)
|
|
: (-totalCost).toFixed(2);
|
|
|
|
return {
|
|
...school,
|
|
totalTuition : totalTuition.toFixed(2),
|
|
monthlyPayment: pmtMin.toFixed(2),
|
|
totalMonthlyPayment: pmt.toFixed(2),
|
|
totalLoanCost : totalCost.toFixed(2),
|
|
netGain,
|
|
monthlySalary : (salary/12).toFixed(2),
|
|
};
|
|
});
|
|
|
|
setResults?.(results);
|
|
setLoading?.(false);
|
|
};
|
|
|
|
/* ================================================================= */
|
|
/* QUICK-FILL PANEL (only when schools.length === 0) */
|
|
/* ================================================================= */
|
|
if (!schools || schools.length === 0) {
|
|
const ready = scratch.tuition;
|
|
return (
|
|
<div className="border rounded p-6 space-y-4 max-w-md mx-auto">
|
|
<h2 className="text-lg font-semibold text-center">
|
|
Estimate student-loan payments
|
|
</h2>
|
|
|
|
<input
|
|
className="border rounded p-2 w-full"
|
|
placeholder="School name *"
|
|
value={scratch.school}
|
|
onChange={(e)=>setScratch({...scratch,school:e.target.value})}
|
|
/>
|
|
|
|
<select
|
|
className="border rounded p-2 w-full"
|
|
value={scratch.programType}
|
|
onChange={(e)=>setScratch({...scratch,programType:e.target.value})}
|
|
>
|
|
<option value="">Degree / program type *</option>
|
|
<option>Associate's Degree</option>
|
|
<option>Bachelor's Degree</option>
|
|
<option>Master's Degree</option>
|
|
<option>Doctoral Degree</option>
|
|
<option>Graduate/Professional Certificate</option>
|
|
<option>First Professional Degree</option>
|
|
</select>
|
|
|
|
<input
|
|
type="number"
|
|
className="border rounded p-2 w-full"
|
|
placeholder="Estimated annual tuition *"
|
|
value={scratch.tuition}
|
|
onChange={(e)=>setScratch({...scratch,tuition:e.target.value})}
|
|
/>
|
|
|
|
<Button
|
|
className="w-full"
|
|
disabled={!ready}
|
|
onClick={()=>{
|
|
const t = Number(scratch.tuition);
|
|
const stub = {
|
|
INSTNM : scratch.school || '(self-entered)', // ← if blank
|
|
degreeType: scratch.programType || 'Unknown', // ← if blank
|
|
programLength: 4,
|
|
inState: t,
|
|
outOfState: t,
|
|
inStateGraduate: t,
|
|
outStateGraduate: t,
|
|
};
|
|
setSchools([stub]); // now main form will appear
|
|
}}
|
|
>
|
|
Continue →
|
|
</Button>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
/* ================================================================= */
|
|
/* ORIGINAL DETAILED REPAYMENT FORM */
|
|
/* ================================================================= */
|
|
return (
|
|
<div className="loan-repayment-container max-w-xl mx-auto">
|
|
<form onSubmit={(e)=>{e.preventDefault();calculateLoanDetails();}}
|
|
className="space-y-4 border rounded p-6">
|
|
{/* Tuition type */}
|
|
<div className="input-group">
|
|
<label className="block font-medium">Tuition Type</label>
|
|
<select
|
|
value={tuitionType}
|
|
onChange={(e)=>setTuitionType(e.target.value)}
|
|
className="border rounded p-2 w-full"
|
|
>
|
|
<option value="inState">In-State</option>
|
|
<option value="outOfState">Out-of-State</option>
|
|
</select>
|
|
</div>
|
|
|
|
{/* Interest rate */}
|
|
<div className="input-group">
|
|
<label className="block font-medium">Interest Rate (%)</label>
|
|
<input type="number"
|
|
value={interestRate}
|
|
onChange={(e)=>setInterestRate(e.target.value)}
|
|
className="border rounded p-2 w-full"/>
|
|
</div>
|
|
|
|
{/* Loan term */}
|
|
<div className="input-group">
|
|
<label className="block font-medium">Loan Term (years)</label>
|
|
<input type="number"
|
|
value={loanTerm}
|
|
onChange={(e)=>setLoanTerm(e.target.value)}
|
|
className="border rounded p-2 w-full"/>
|
|
</div>
|
|
|
|
{/* Extra payment */}
|
|
<div className="input-group">
|
|
<label className="block font-medium">Extra Monthly Payment</label>
|
|
<input type="number"
|
|
value={extraPayment}
|
|
onChange={(e)=>setExtraPayment(e.target.value)}
|
|
className="border rounded p-2 w-full"/>
|
|
</div>
|
|
|
|
{/* Current salary */}
|
|
<div className="input-group">
|
|
<label className="block font-medium">Current Salary</label>
|
|
<input type="number"
|
|
value={currentSalary}
|
|
onChange={(e)=>setCurrentSalary(e.target.value)}
|
|
className="border rounded p-2 w-full"/>
|
|
</div>
|
|
|
|
{/* Expected salary */}
|
|
<div className="input-group">
|
|
<label className="block font-medium">Expected Salary</label>
|
|
<input type="number"
|
|
value={expectedSalary}
|
|
onChange={(e)=>setExpectedSalary(e.target.value)}
|
|
className="border rounded p-2 w-full"/>
|
|
</div>
|
|
|
|
{/* Submit */}
|
|
<div className="text-right">
|
|
<Button type="submit">Calculate</Button>
|
|
</div>
|
|
|
|
{error && <div className="text-red-600 text-sm">{error}</div>}
|
|
</form>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default LoanRepayment;
|