Added manual task manipulation, fixed UI
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
This commit is contained in:
parent
e6d567d839
commit
893757646b
@ -1 +1 @@
|
|||||||
b632ad41cfb05900be9a667c396e66a4dfb26320-22db19df6f582b90837f001b7bf6ead59acca441-e9eccd451b778829eb2f2c9752c670b707e1268b
|
afd62e0deab27814cfa0067f1fae1dc4ad79e7dd-22db19df6f582b90837f001b7bf6ead59acca441-e9eccd451b778829eb2f2c9752c670b707e1268b
|
||||||
|
@ -1,61 +1,59 @@
|
|||||||
|
// src/components/MilestoneDrawer.js
|
||||||
import React, { useMemo, useState, useEffect } from 'react';
|
import React, { useMemo, useState, useEffect } from 'react';
|
||||||
import { Button } from './ui/button.js';
|
import { Button } from './ui/button.js';
|
||||||
import { Card, CardContent } from './ui/card.js';
|
import { Card, CardContent } from './ui/card.js';
|
||||||
import { ChevronLeft, Check, Loader2 } from 'lucide-react';
|
import { ChevronLeft, Check, Trash2, PencilLine, X } from 'lucide-react';
|
||||||
import { flattenTasks } from '../utils/taskHelpers.js';
|
import { flattenTasks } from '../utils/taskHelpers.js';
|
||||||
import authFetch from '../utils/authFetch.js';
|
import authFetch from '../utils/authFetch.js';
|
||||||
import format from 'date-fns/format';
|
import format from 'date-fns/format';
|
||||||
|
|
||||||
/* simple status → color map */
|
|
||||||
const pillStyle = {
|
const pillStyle = {
|
||||||
completed : 'bg-green-100 text-green-800',
|
completed : 'bg-green-100 text-green-800',
|
||||||
in_progress : 'bg-blue-100 text-blue-800',
|
in_progress : 'bg-blue-100 text-blue-800',
|
||||||
not_started : 'bg-gray-100 text-gray-700'
|
not_started : 'bg-gray-100 text-gray-700'
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const statusLabel = {
|
||||||
|
not_started : 'Not started',
|
||||||
|
in_progress : 'In progress',
|
||||||
|
completed : 'Completed'
|
||||||
|
};
|
||||||
|
|
||||||
|
const nextStatus = { not_started:'in_progress', in_progress:'completed', completed:'not_started' };
|
||||||
|
|
||||||
export default function MilestoneDrawer({
|
export default function MilestoneDrawer({
|
||||||
milestone, // ← pass a single milestone object
|
milestone, // single milestone object
|
||||||
milestones = [], // still needed to compute progress %
|
milestones = [], // still available if you compute progress elsewhere
|
||||||
open,
|
open,
|
||||||
onClose,
|
onClose,
|
||||||
onTaskToggle = () => {}
|
onTaskToggle = () => {}
|
||||||
}) {
|
}) {
|
||||||
|
// Local task list (flatten if your milestone.tasks has nested shape)
|
||||||
|
const [tasks, setTasks] = useState(milestone ? flattenTasks([milestone]) : []);
|
||||||
|
const [adding, setAdding] = useState(false);
|
||||||
|
const [editingId, setEditingId] = useState(null);
|
||||||
|
|
||||||
/* gather tasks progress for this milestone */
|
const [draftNew, setDraftNew] = useState({ title:'', due_date:'', description:'' });
|
||||||
const [tasks, setTasks] = useState(
|
const [draftEdit, setDraftEdit] = useState({ title:'', due_date:'', description:'' });
|
||||||
milestone ? flattenTasks([milestone]) : []
|
|
||||||
);
|
|
||||||
|
|
||||||
// refresh local copy whenever the user selects a different milestone
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setTasks(milestone ? flattenTasks([milestone]) : []);
|
setTasks(milestone ? flattenTasks([milestone]) : []);
|
||||||
|
setAdding(false);
|
||||||
|
setEditingId(null);
|
||||||
|
setDraftNew({ title:'', due_date:'', description:'' });
|
||||||
}, [milestone]);
|
}, [milestone]);
|
||||||
|
|
||||||
|
if (!open || !milestone) return null;
|
||||||
|
|
||||||
const done = tasks.filter(t => t.status === 'completed').length;
|
const done = tasks.filter(t => t.status === 'completed').length;
|
||||||
const prog = tasks.length ? Math.round(100 * done / tasks.length) : 0;
|
const prog = tasks.length ? Math.round(100 * done / tasks.length) : 0;
|
||||||
|
|
||||||
if (!open || !milestone) return null;
|
|
||||||
|
|
||||||
async function toggle(t) {
|
async function toggle(t) {
|
||||||
|
const newStatus = nextStatus[t.status] || 'not_started';
|
||||||
|
|
||||||
const next = {
|
// optimistic local update
|
||||||
not_started : 'in_progress',
|
setTasks(prev => prev.map(x => x.id === t.id ? { ...x, status:newStatus } : x));
|
||||||
in_progress : 'completed',
|
onTaskToggle(t.id, newStatus);
|
||||||
completed : 'not_started' // undo
|
|
||||||
};
|
|
||||||
|
|
||||||
const newStatus = next[t.status] || 'not_started';
|
|
||||||
|
|
||||||
|
|
||||||
/* 1️⃣ optimistic local update */
|
|
||||||
setTasks(prev =>
|
|
||||||
prev.map(x =>
|
|
||||||
x.id === t.id ? { ...x, status: newStatus } : x
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
/* 2️⃣ inform parent so progress bars refresh elsewhere */
|
|
||||||
onTaskToggle(t.id, newStatus);
|
|
||||||
|
|
||||||
await authFetch(`/api/premium/tasks/${t.id}`, {
|
await authFetch(`/api/premium/tasks/${t.id}`, {
|
||||||
method : 'PUT',
|
method : 'PUT',
|
||||||
@ -64,11 +62,82 @@ const newStatus = next[t.status] || 'not_started';
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const statusLabel = {
|
async function createTask() {
|
||||||
not_started : 'Not started',
|
const { title, due_date, description } = draftNew;
|
||||||
in_progress : 'In progress',
|
if (!title.trim()) return;
|
||||||
completed : 'Completed'
|
|
||||||
};
|
const body = {
|
||||||
|
milestone_id: milestone.id,
|
||||||
|
title: title.trim(),
|
||||||
|
description: description || '',
|
||||||
|
due_date: due_date || null,
|
||||||
|
status: 'not_started'
|
||||||
|
};
|
||||||
|
|
||||||
|
// optimistic add (temporary id)
|
||||||
|
const tempId = `tmp-${Date.now()}`;
|
||||||
|
setTasks(prev => [...prev, { ...body, id: tempId }]);
|
||||||
|
setDraftNew({ title:'', due_date:'', description:'' });
|
||||||
|
setAdding(false);
|
||||||
|
|
||||||
|
const res = await authFetch('/api/premium/tasks', {
|
||||||
|
method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify(body)
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.ok) {
|
||||||
|
const saved = await res.json();
|
||||||
|
const real = Array.isArray(saved) ? saved[0] : saved;
|
||||||
|
// replace temp id with real id
|
||||||
|
setTasks(prev => prev.map(t => t.id === tempId ? { ...t, id: real.id } : t));
|
||||||
|
} else {
|
||||||
|
// rollback on failure
|
||||||
|
setTasks(prev => prev.filter(t => t.id !== tempId));
|
||||||
|
alert(await res.text());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function beginEdit(t) {
|
||||||
|
setEditingId(t.id);
|
||||||
|
setDraftEdit({
|
||||||
|
title: t.title || '',
|
||||||
|
due_date: t.due_date ? String(t.due_date).slice(0,10) : '',
|
||||||
|
description: t.description || ''
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function cancelEdit() {
|
||||||
|
setEditingId(null);
|
||||||
|
setDraftEdit({ title:'', due_date:'', description:'' });
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveEdit(id) {
|
||||||
|
const body = {
|
||||||
|
title: (draftEdit.title || '').trim(),
|
||||||
|
description: draftEdit.description || '',
|
||||||
|
due_date: draftEdit.due_date || null
|
||||||
|
};
|
||||||
|
if (!body.title) return;
|
||||||
|
|
||||||
|
// optimistic local update
|
||||||
|
setTasks(prev => prev.map(t => t.id === id ? { ...t, ...body } : t));
|
||||||
|
setEditingId(null);
|
||||||
|
|
||||||
|
const res = await authFetch(`/api/premium/tasks/${id}`, {
|
||||||
|
method:'PUT', headers:{'Content-Type':'application/json'}, body: JSON.stringify(body)
|
||||||
|
});
|
||||||
|
if (!res.ok) alert(await res.text());
|
||||||
|
}
|
||||||
|
|
||||||
|
async function remove(id) {
|
||||||
|
// optimistic local delete
|
||||||
|
const prev = tasks;
|
||||||
|
setTasks(prev.filter(t => t.id !== id));
|
||||||
|
const res = await authFetch(`/api/premium/tasks/${id}`, { method:'DELETE' });
|
||||||
|
if (!res.ok) {
|
||||||
|
alert(await res.text());
|
||||||
|
setTasks(prev); // rollback
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="fixed inset-y-0 right-0 w-full max-w-sm bg-white shadow-xl z-40 flex flex-col">
|
<div className="fixed inset-y-0 right-0 w-full max-w-sm bg-white shadow-xl z-40 flex flex-col">
|
||||||
@ -85,57 +154,135 @@ const newStatus = next[t.status] || 'not_started';
|
|||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
<Button size="sm" onClick={() => setAdding(a => !a)}>
|
||||||
|
{adding ? 'Cancel' : '+ Add task'}
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Body */}
|
{/* Body */}
|
||||||
<Card className="flex-1 overflow-y-auto rounded-none">
|
<Card className="flex-1 overflow-y-auto rounded-none">
|
||||||
<CardContent className="space-y-4">
|
<CardContent className="space-y-4">
|
||||||
|
|
||||||
{/* Progress bar */}
|
{/* Progress */}
|
||||||
<div>
|
<div>
|
||||||
<progress value={prog} max={100} className="w-full h-2" />
|
<progress value={prog} max={100} className="w-full h-2" />
|
||||||
<p className="text-xs text-gray-500 mt-1">{prog}% complete</p>
|
<p className="text-xs text-gray-500 mt-1">{prog}% complete</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Add composer */}
|
||||||
|
{adding && (
|
||||||
|
<div className="border rounded-lg p-3 space-y-2">
|
||||||
|
<div>
|
||||||
|
<label className="label-xs">Title</label>
|
||||||
|
<input
|
||||||
|
className="input w-full"
|
||||||
|
value={draftNew.title}
|
||||||
|
onChange={e => setDraftNew(d => ({ ...d, title:e.target.value }))}
|
||||||
|
placeholder="What needs to be done?"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-2 gap-2">
|
||||||
|
<div>
|
||||||
|
<label className="label-xs">Due date</label>
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
className="input w-full"
|
||||||
|
value={draftNew.due_date}
|
||||||
|
onChange={e => setDraftNew(d => ({ ...d, due_date:e.target.value }))}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="label-xs">Status</label>
|
||||||
|
<input className="input w-full" value="Not started" disabled />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="label-xs">Description (optional)</label>
|
||||||
|
<textarea
|
||||||
|
rows={2}
|
||||||
|
className="input w-full resize-none"
|
||||||
|
value={draftNew.description}
|
||||||
|
onChange={e => setDraftNew(d => ({ ...d, description:e.target.value }))}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="text-right">
|
||||||
|
<Button onClick={createTask}>Save task</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Task list */}
|
{/* Task list */}
|
||||||
{tasks.map(t => (
|
{tasks.map(t => (
|
||||||
<div
|
<div key={t.id} className="border p-3 rounded-lg space-y-2">
|
||||||
key={t.id}
|
{editingId === t.id ? (
|
||||||
className="border p-3 rounded-lg flex items-start justify-between"
|
<>
|
||||||
>
|
<div>
|
||||||
<div className="pr-2">
|
<label className="label-xs">Title</label>
|
||||||
<p className="font-medium break-words">{t.title}</p>
|
<input
|
||||||
{t.due_date && (
|
className="input w-full"
|
||||||
<p className="text-xs text-gray-500">
|
value={draftEdit.title}
|
||||||
{format(new Date(t.due_date), 'PP')}
|
onChange={e => setDraftEdit(d => ({ ...d, title:e.target.value }))}
|
||||||
</p>
|
/>
|
||||||
)}
|
</div>
|
||||||
</div>
|
<div className="grid grid-cols-2 gap-2">
|
||||||
|
<div>
|
||||||
|
<label className="label-xs">Due date</label>
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
className="input w-full"
|
||||||
|
value={draftEdit.due_date}
|
||||||
|
onChange={e => setDraftEdit(d => ({ ...d, due_date:e.target.value }))}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="label-xs">Status</label>
|
||||||
|
<input className="input w-full" value={statusLabel[t.status]} disabled />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="label-xs">Description</label>
|
||||||
|
<textarea
|
||||||
|
rows={2}
|
||||||
|
className="input w-full resize-none"
|
||||||
|
value={draftEdit.description}
|
||||||
|
onChange={e => setDraftEdit(d => ({ ...d, description:e.target.value }))}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-end gap-2">
|
||||||
|
<Button variant="secondary" onClick={cancelEdit}>Cancel</Button>
|
||||||
|
<Button onClick={() => saveEdit(t.id)}>Save</Button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<div className="flex items-start justify-between gap-3">
|
||||||
|
<div className="pr-2 min-w-0">
|
||||||
|
<p className="font-medium break-words">{t.title}</p>
|
||||||
|
<div className="text-xs text-gray-500 space-x-2">
|
||||||
|
{t.due_date && <span>{format(new Date(t.due_date), 'PP')}</span>}
|
||||||
|
{t.description && <span className="block text-gray-600">{t.description}</span>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<Button
|
<div className="flex items-center gap-1 shrink-0">
|
||||||
size="icon"
|
<Button size="icon" variant="ghost" aria-label="Toggle" onClick={() => toggle(t)}>
|
||||||
variant="ghost"
|
{t.status === 'completed'
|
||||||
aria-label="Toggle task status"
|
? <Check className="w-5 h-5 text-green-600" />
|
||||||
onClick={() => toggle(t)}
|
: <span className={`inline-block px-2 py-0.5 rounded-full text-[10px] font-semibold ${pillStyle[t.status]}`} style={{whiteSpace:'nowrap'}}>{statusLabel[t.status]}</span>}
|
||||||
>
|
</Button>
|
||||||
{t.status === 'completed'
|
<Button size="icon" variant="outline" aria-label="Edit" onClick={() => beginEdit(t)}>
|
||||||
? <Check className="w-5 h-5 text-green-600" />
|
<PencilLine className="w-5 h-5 text-white" />
|
||||||
: (
|
</Button>
|
||||||
<span
|
<Button size="icon" className="bg-rose-600 hover:bg-rose-700 text-white" onClick={() => remove(t.id)}>
|
||||||
className={`shrink-0 inline-block px-2 py-0.5 rounded-full text-[10px] font-semibold ${pillStyle[t.status]}`} style={{ whiteSpace: 'nowrap' }} /* never wrap */
|
<Trash2 className="w-5 h-5" />
|
||||||
>
|
</Button>
|
||||||
{statusLabel[t.status]}
|
</div>
|
||||||
</span>
|
</div>
|
||||||
)
|
)}
|
||||||
}
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
{!tasks.length && (
|
{!tasks.length && !adding && (
|
||||||
<p className="text-sm text-gray-500">
|
<p className="text-sm text-gray-500">No tasks have been added to this milestone yet.</p>
|
||||||
No tasks have been added to this milestone yet.
|
|
||||||
</p>
|
|
||||||
)}
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -1,6 +1,7 @@
|
|||||||
// CareerOnboarding.js
|
// CareerOnboarding.js
|
||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { useLocation } from 'react-router-dom';
|
import { useLocation } from 'react-router-dom';
|
||||||
|
import { saveDraft, clearDraft, loadDraft } from '../../utils/onboardingDraftApi.js';
|
||||||
|
|
||||||
// 1) Import your CareerSearch component
|
// 1) Import your CareerSearch component
|
||||||
import CareerSearch from '../CareerSearch.js'; // adjust path as necessary
|
import CareerSearch from '../CareerSearch.js'; // adjust path as necessary
|
||||||
@ -51,14 +52,20 @@ const CareerOnboarding = ({ nextStep, prevStep, data, setData, finishNow }) => {
|
|||||||
// Called whenever other <inputs> change
|
// Called whenever other <inputs> change
|
||||||
const handleChange = (e) => {
|
const handleChange = (e) => {
|
||||||
setData(prev => ({ ...prev, [e.target.name]: e.target.value }));
|
setData(prev => ({ ...prev, [e.target.name]: e.target.value }));
|
||||||
|
const k = e.target.name;
|
||||||
|
if (['status','start_date','career_goals'].includes(k)) {
|
||||||
|
saveDraft({ careerData: { [k]: e.target.value } }).catch(() => {});
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/* ── 4. callbacks ─────────────────────────────────────────── */
|
/* ── 4. callbacks ─────────────────────────────────────────── */
|
||||||
function handleCareerSelected(obj) {
|
function handleCareerSelected(career) {
|
||||||
setCareerObj(obj);
|
setCareerObj(career);
|
||||||
localStorage.setItem('selectedCareer', JSON.stringify(obj));
|
localStorage.setItem('selectedCareer', JSON.stringify(career));
|
||||||
setData(prev => ({ ...prev, career_name: obj.title, soc_code: obj.soc_code || '' }));
|
setData(prev => ({ ...prev, career_name: career.title, soc_code: career.soc_code || '' }));
|
||||||
}
|
// persist immediately
|
||||||
|
saveDraft({ careerData: { career_name: career.title, soc_code: career.soc_code || '' } }).catch(() => {});
|
||||||
|
}
|
||||||
|
|
||||||
function handleSubmit() {
|
function handleSubmit() {
|
||||||
if (!ready) return alert('Fill all required fields.');
|
if (!ready) return alert('Fill all required fields.');
|
||||||
@ -95,8 +102,10 @@ const nextLabel = !skipFin ? 'Financial →' : (inCollege ? 'College →' : 'Fin
|
|||||||
<select
|
<select
|
||||||
value={currentlyWorking}
|
value={currentlyWorking}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
setCurrentlyWorking(e.target.value);
|
const val = e.target.value;
|
||||||
setData(prev => ({ ...prev, currently_working: e.target.value }));
|
setCurrentlyWorking(val);
|
||||||
|
setData(prev => ({ ...prev, currently_working: val }));
|
||||||
|
saveDraft({ careerData: { currently_working: val} }).catch(() => {});
|
||||||
}}
|
}}
|
||||||
required
|
required
|
||||||
className="w-full border rounded p-2"
|
className="w-full border rounded p-2"
|
||||||
@ -158,10 +167,11 @@ const nextLabel = !skipFin ? 'Financial →' : (inCollege ? 'College →' : 'Fin
|
|||||||
<select
|
<select
|
||||||
value={collegeStatus}
|
value={collegeStatus}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
setCollegeStatus(e.target.value);
|
const val = e.target.value;
|
||||||
setData(prev => ({ ...prev, college_enrollment_status: e.target.value }));
|
setCurrentlyWorking(val);
|
||||||
const needsPrompt = ['currently_enrolled', 'prospective_student'].includes(e.target.value);
|
setData(prev => ({ ...prev, currently_working: val }));
|
||||||
setShowFinPrompt(needsPrompt);
|
// persist immediately
|
||||||
|
saveDraft({ careerData: { currently_working: val } }).catch(() => {});
|
||||||
}}
|
}}
|
||||||
required
|
required
|
||||||
className="w-full border rounded p-2"
|
className="w-full border rounded p-2"
|
||||||
|
@ -3,6 +3,7 @@ import Modal from '../../components/ui/modal.js';
|
|||||||
import FinancialAidWizard from '../../components/FinancialAidWizard.js';
|
import FinancialAidWizard from '../../components/FinancialAidWizard.js';
|
||||||
import { useLocation } from 'react-router-dom';
|
import { useLocation } from 'react-router-dom';
|
||||||
import api from '../../auth/apiClient.js';
|
import api from '../../auth/apiClient.js';
|
||||||
|
import { loadDraft, clearDraft, saveDraft } from '../../utils/onboardingDraftApi.js';
|
||||||
|
|
||||||
const Req = () => <span className="text-red-600 ml-0.5">*</span>;
|
const Req = () => <span className="text-red-600 ml-0.5">*</span>;
|
||||||
|
|
||||||
@ -57,7 +58,7 @@ function toSchoolName(objOrStr) {
|
|||||||
// Destructure parent data
|
// Destructure parent data
|
||||||
const {
|
const {
|
||||||
college_enrollment_status = '',
|
college_enrollment_status = '',
|
||||||
selected_school = selectedSchool,
|
selected_school = '',
|
||||||
selected_program = '',
|
selected_program = '',
|
||||||
program_type = '',
|
program_type = '',
|
||||||
academic_calendar = 'semester',
|
academic_calendar = 'semester',
|
||||||
@ -99,6 +100,40 @@ function toSchoolName(objOrStr) {
|
|||||||
}
|
}
|
||||||
}, [selectedSchool, setData]);
|
}, [selectedSchool, setData]);
|
||||||
|
|
||||||
|
// Backfill from cookie-backed draft if props aren't populated yet
|
||||||
|
useEffect(() => {
|
||||||
|
// if props already have values, do nothing
|
||||||
|
if (data?.selected_school || data?.selected_program || data?.program_type) return;
|
||||||
|
|
||||||
|
let cancelled = false;
|
||||||
|
(async () => {
|
||||||
|
let draft;
|
||||||
|
try { draft = await loadDraft(); } catch { draft = null; }
|
||||||
|
const cd = draft?.data?.collegeData;
|
||||||
|
if (!cd) return;
|
||||||
|
if (cancelled) return;
|
||||||
|
|
||||||
|
// 1) write into parent data (so inputs prefill)
|
||||||
|
setData(prev => ({
|
||||||
|
...prev,
|
||||||
|
selected_school : cd.selected_school ?? prev.selected_school ?? '',
|
||||||
|
selected_program: cd.selected_program ?? prev.selected_program ?? '',
|
||||||
|
program_type : cd.program_type ?? prev.program_type ?? ''
|
||||||
|
}));
|
||||||
|
|
||||||
|
// 2) set local selectedSchool object (triggers your selectedSchool→data effect too)
|
||||||
|
setSelectedSchool({
|
||||||
|
INSTNM : cd.selected_school || '',
|
||||||
|
CIPDESC : cd.selected_program || '',
|
||||||
|
CREDDESC: cd.program_type || ''
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
|
||||||
|
return () => { cancelled = true; };
|
||||||
|
// run once on mount; we don't want to fight subsequent user edits
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (data.expected_graduation && !expectedGraduation)
|
if (data.expected_graduation && !expectedGraduation)
|
||||||
setExpectedGraduation(data.expected_graduation);
|
setExpectedGraduation(data.expected_graduation);
|
||||||
@ -109,40 +144,40 @@ useEffect(() => {
|
|||||||
* If user leaves numeric fields blank, store '' in local state, not 0.
|
* If user leaves numeric fields blank, store '' in local state, not 0.
|
||||||
* Only parseFloat if there's an actual numeric value.
|
* Only parseFloat if there's an actual numeric value.
|
||||||
*/
|
*/
|
||||||
const handleParentFieldChange = (e) => {
|
const handleParentFieldChange = (e) => {
|
||||||
const { name, value, type, checked } = e.target;
|
const { name: field, value, type, checked } = e.target;
|
||||||
let val = value;
|
let val = value;
|
||||||
|
|
||||||
if (type === 'checkbox') {
|
if (type === 'checkbox') {
|
||||||
val = checked;
|
val = checked;
|
||||||
setData(prev => ({ ...prev, [name]: val }));
|
setData(prev => ({ ...prev, [field]: val }));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the user typed an empty string, store '' so they can see it's blank
|
// If the user typed an empty string, store '' so they can see it's blank
|
||||||
if (val.trim() === '') {
|
if (val.trim() === '') {
|
||||||
setData(prev => ({ ...prev, [name]: '' }));
|
setData(prev => ({ ...prev, [field]: '' }));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Otherwise, parse it if it's one of the numeric fields
|
// Otherwise, parse it if it's one of the numeric fields
|
||||||
if (['interest_rate', 'loan_term', 'extra_payment', 'expected_salary'].includes(name)) {
|
if (['interest_rate', 'loan_term', 'extra_payment', 'expected_salary'].includes(field)) {
|
||||||
const parsed = parseFloat(val);
|
const parsed = parseFloat(val);
|
||||||
// If parse fails => store '' (or fallback to old value)
|
// If parse fails => store '' (or fallback to old value)
|
||||||
if (isNaN(parsed)) {
|
if (isNaN(parsed)) {
|
||||||
setData(prev => ({ ...prev, [name]: '' }));
|
setData(prev => ({ ...prev, [field]: '' }));
|
||||||
} else {
|
} else {
|
||||||
setData(prev => ({ ...prev, [name]: parsed }));
|
setData(prev => ({ ...prev, [field]: parsed }));
|
||||||
}
|
}
|
||||||
} else if ([
|
} else if ([
|
||||||
'annual_financial_aid','existing_college_debt','credit_hours_per_year',
|
'annual_financial_aid','existing_college_debt','credit_hours_per_year',
|
||||||
'hours_completed','credit_hours_required','tuition_paid'
|
'hours_completed','credit_hours_required','tuition_paid'
|
||||||
].includes(name)) {
|
].includes(field)) {
|
||||||
const parsed = parseFloat(val);
|
const parsed = parseFloat(val);
|
||||||
setData(prev => ({ ...prev, [name]: isNaN(parsed) ? '' : parsed }));
|
setData(prev => ({ ...prev, [field]: isNaN(parsed) ? '' : parsed }));
|
||||||
} else {
|
} else {
|
||||||
// For non-numeric or strings
|
// For non-numeric or strings
|
||||||
setData(prev => ({ ...prev, [name]: val }));
|
setData(prev => ({ ...prev, [field]: val }));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -202,6 +237,7 @@ useEffect(() => {
|
|||||||
setSchoolSuggestions([]);
|
setSchoolSuggestions([]);
|
||||||
setProgramSuggestions([]);
|
setProgramSuggestions([]);
|
||||||
setAvailableProgramTypes([]);
|
setAvailableProgramTypes([]);
|
||||||
|
saveDraft({ collegeData: { selected_school: name } }).catch(() => {});
|
||||||
};
|
};
|
||||||
|
|
||||||
// Program
|
// Program
|
||||||
@ -222,6 +258,7 @@ useEffect(() => {
|
|||||||
const handleProgramSelect = (prog) => {
|
const handleProgramSelect = (prog) => {
|
||||||
setData(prev => ({ ...prev, selected_program: prog }));
|
setData(prev => ({ ...prev, selected_program: prog }));
|
||||||
setProgramSuggestions([]);
|
setProgramSuggestions([]);
|
||||||
|
saveDraft({ collegeData: { selected_program: prog } }).catch(() => {});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleProgramTypeSelect = (e) => {
|
const handleProgramTypeSelect = (e) => {
|
||||||
@ -233,6 +270,7 @@ useEffect(() => {
|
|||||||
}));
|
}));
|
||||||
setManualProgramLength('');
|
setManualProgramLength('');
|
||||||
setAutoProgramLength('0.00');
|
setAutoProgramLength('0.00');
|
||||||
|
saveDraft({ collegeData: { program_type: val } }).catch(() => {});
|
||||||
};
|
};
|
||||||
|
|
||||||
// once we have school+program => load possible program types
|
// once we have school+program => load possible program types
|
||||||
@ -305,6 +343,26 @@ useEffect(() => {
|
|||||||
credit_hours_required,
|
credit_hours_required,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const hasSchool = !!data.selected_school;
|
||||||
|
const hasAnyProgram = !!data.selected_program || !!data.program_type;
|
||||||
|
if (!hasSchool && !hasAnyProgram) return;
|
||||||
|
|
||||||
|
setSelectedSchool(prev => {
|
||||||
|
const next = {
|
||||||
|
INSTNM : data.selected_school || '',
|
||||||
|
CIPDESC : data.selected_program || '',
|
||||||
|
CREDDESC: data.program_type || ''
|
||||||
|
};
|
||||||
|
// avoid useless state churn
|
||||||
|
if (prev &&
|
||||||
|
prev.INSTNM === next.INSTNM &&
|
||||||
|
prev.CIPDESC === next.CIPDESC &&
|
||||||
|
prev.CREDDESC=== next.CREDDESC) return prev;
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
}, [data.selected_school, data.selected_program, data.program_type]);
|
||||||
|
|
||||||
/* ------------------------------------------------------------------ */
|
/* ------------------------------------------------------------------ */
|
||||||
/* Whenever the user changes enrollmentDate OR programLength */
|
/* Whenever the user changes enrollmentDate OR programLength */
|
||||||
/* (program_length is already in parent data), compute grad date. */
|
/* (program_length is already in parent data), compute grad date. */
|
||||||
@ -466,7 +524,7 @@ const ready =
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<label className="block font-medium">Marjor/Program Name* (Please select from drop-down after typing){infoIcon("Search and click from auto-suggest. If for some reason your major isn't listed, please send us a note.")}</label>
|
<label className="block font-medium">Major/Program Name* (Please select from drop-down after typing){infoIcon("Search and click from auto-suggest. If for some reason your major isn't listed, please send us a note.")}</label>
|
||||||
<input
|
<input
|
||||||
name="selected_program"
|
name="selected_program"
|
||||||
value={selected_program}
|
value={selected_program}
|
||||||
|
@ -3,6 +3,7 @@ import React, { useState } from 'react';
|
|||||||
import Modal from '../ui/modal.js';
|
import Modal from '../ui/modal.js';
|
||||||
import ExpensesWizard from '../../components/ExpensesWizard.js'; // path to your wizard
|
import ExpensesWizard from '../../components/ExpensesWizard.js'; // path to your wizard
|
||||||
import { Button } from '../../components/ui/button.js'; // using your Tailwind-based button
|
import { Button } from '../../components/ui/button.js'; // using your Tailwind-based button
|
||||||
|
import { saveDraft, clearDraft, loadDraft } from '../../utils/onboardingDraftApi.js';
|
||||||
|
|
||||||
const FinancialOnboarding = ({ nextStep, prevStep, data, setData }) => {
|
const FinancialOnboarding = ({ nextStep, prevStep, data, setData }) => {
|
||||||
const {
|
const {
|
||||||
@ -26,10 +27,8 @@ const FinancialOnboarding = ({ nextStep, prevStep, data, setData }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleExpensesCalculated = (total) => {
|
const handleExpensesCalculated = (total) => {
|
||||||
setData(prev => ({
|
setData(prev => ({...prev, monthly_expenses: total }));
|
||||||
...prev,
|
saveDraft({ financialData: { monthly_expenses: total } }).catch(() => {});
|
||||||
monthly_expenses: total
|
|
||||||
}));
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const infoIcon = (msg) => (
|
const infoIcon = (msg) => (
|
||||||
@ -55,6 +54,12 @@ const FinancialOnboarding = ({ nextStep, prevStep, data, setData }) => {
|
|||||||
extra_cash_emergency_pct: val,
|
extra_cash_emergency_pct: val,
|
||||||
extra_cash_retirement_pct: 100 - val
|
extra_cash_retirement_pct: 100 - val
|
||||||
}));
|
}));
|
||||||
|
saveDraft({
|
||||||
|
financialData: {
|
||||||
|
extra_cash_emergency_pct: val,
|
||||||
|
extra_cash_retirement_pct: 100 - val
|
||||||
|
}
|
||||||
|
}).catch(() => {});
|
||||||
} else if (name === 'extra_cash_retirement_pct') {
|
} else if (name === 'extra_cash_retirement_pct') {
|
||||||
val = Math.min(Math.max(val, 0), 100);
|
val = Math.min(Math.max(val, 0), 100);
|
||||||
setData(prevData => ({
|
setData(prevData => ({
|
||||||
@ -62,8 +67,20 @@ const FinancialOnboarding = ({ nextStep, prevStep, data, setData }) => {
|
|||||||
extra_cash_retirement_pct: val,
|
extra_cash_retirement_pct: val,
|
||||||
extra_cash_emergency_pct: 100 - val
|
extra_cash_emergency_pct: 100 - val
|
||||||
}));
|
}));
|
||||||
|
saveDraft({
|
||||||
|
financialData: {
|
||||||
|
extra_cash_emergency_pct: val,
|
||||||
|
extra_cash_retirement_pct: 100 - val
|
||||||
|
}
|
||||||
|
}).catch(() => {});
|
||||||
} else {
|
} else {
|
||||||
setData(prevData => ({ ...prevData, [name]: val }));
|
setData(prevData => ({ ...prevData, [name]: val }));
|
||||||
|
saveDraft({
|
||||||
|
financialData: {
|
||||||
|
extra_cash_emergency_pct: val,
|
||||||
|
extra_cash_retirement_pct: 100 - val
|
||||||
|
}
|
||||||
|
}).catch(() => {});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user