fix(assignments): resolve op.student → res.users + student search
E2E QA surfaced a silent-drop bug in the exam-schedule endpoint: the frontend's student picker sends `op.student.id` values (that's what /api/students returns), but `encoach.exam.schedule.student_ids` is a Many2many → res.users. The controller was writing the op.student id straight through, so schedules linked to whichever res.users row happened to share that integer id (OdooBot, other staff, etc.) — the target student got 0 assignments and never saw the exam. - exam_schedules.py: add `_resolve_student_user_ids` and use it in the create and update endpoints. Falls back to treating unmatched ids as direct res.users ids for back-office callers. - AssignmentsPage.tsx: add a search input + larger list to the Individual Students picker; long lists were leaving Sarah unreachable inside the inner scroll container during QA. Made-with: Cursor
This commit is contained in:
@@ -114,6 +114,7 @@ export default function AssignmentsPage() {
|
|||||||
const [stateFilter, setStateFilter] = useState<ScheduleState | "all">("all");
|
const [stateFilter, setStateFilter] = useState<ScheduleState | "all">("all");
|
||||||
const [createOpen, setCreateOpen] = useState(false);
|
const [createOpen, setCreateOpen] = useState(false);
|
||||||
const [form, setForm] = useState<FormState>(emptyForm());
|
const [form, setForm] = useState<FormState>(emptyForm());
|
||||||
|
const [studentQuery, setStudentQuery] = useState("");
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const qc = useQueryClient();
|
const qc = useQueryClient();
|
||||||
|
|
||||||
@@ -144,6 +145,14 @@ export default function AssignmentsPage() {
|
|||||||
|
|
||||||
const studentsQ = useStudents({ size: 200 });
|
const studentsQ = useStudents({ size: 200 });
|
||||||
const students = studentsQ.data?.items ?? [];
|
const students = studentsQ.data?.items ?? [];
|
||||||
|
const filteredStudents = useMemo(() => {
|
||||||
|
const q = studentQuery.trim().toLowerCase();
|
||||||
|
if (!q) return students;
|
||||||
|
return students.filter((s) =>
|
||||||
|
(s.name || "").toLowerCase().includes(q) ||
|
||||||
|
(s.email || "").toLowerCase().includes(q),
|
||||||
|
);
|
||||||
|
}, [students, studentQuery]);
|
||||||
|
|
||||||
const stateCounts = useMemo(() => {
|
const stateCounts = useMemo(() => {
|
||||||
const all = schedulesQ.data?.items ?? [];
|
const all = schedulesQ.data?.items ?? [];
|
||||||
@@ -468,9 +477,23 @@ export default function AssignmentsPage() {
|
|||||||
{/* Individual student selection */}
|
{/* Individual student selection */}
|
||||||
{form.assign_mode === "individual" && (
|
{form.assign_mode === "individual" && (
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
|
<div className="flex items-center justify-between gap-2">
|
||||||
<Label className="text-xs">Select Students</Label>
|
<Label className="text-xs">Select Students</Label>
|
||||||
<div className="max-h-48 overflow-y-auto border rounded-md p-2 space-y-1">
|
<span className="text-[11px] text-muted-foreground">
|
||||||
{students.map((s) => (
|
{form.student_ids.size} selected · {students.length} total
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{/* Search box — needed because QA reported long student lists
|
||||||
|
made bottom entries unreachable inside the inner scroll
|
||||||
|
container. Filtering keeps the list short and predictable. */}
|
||||||
|
<Input
|
||||||
|
placeholder="Search students by name or email…"
|
||||||
|
value={studentQuery}
|
||||||
|
onChange={(e) => setStudentQuery(e.target.value)}
|
||||||
|
className="h-8 text-xs"
|
||||||
|
/>
|
||||||
|
<div className="max-h-64 overflow-y-auto border rounded-md p-2 space-y-1">
|
||||||
|
{filteredStudents.map((s) => (
|
||||||
<label key={s.id} className="flex items-center gap-2 text-sm cursor-pointer hover:bg-muted/50 rounded px-1 py-0.5">
|
<label key={s.id} className="flex items-center gap-2 text-sm cursor-pointer hover:bg-muted/50 rounded px-1 py-0.5">
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={form.student_ids.has(s.id)}
|
checked={form.student_ids.has(s.id)}
|
||||||
@@ -487,6 +510,11 @@ export default function AssignmentsPage() {
|
|||||||
{s.email && <span className="text-xs text-muted-foreground ml-auto">{s.email}</span>}
|
{s.email && <span className="text-xs text-muted-foreground ml-auto">{s.email}</span>}
|
||||||
</label>
|
</label>
|
||||||
))}
|
))}
|
||||||
|
{filteredStudents.length === 0 && students.length > 0 && (
|
||||||
|
<p className="text-xs text-muted-foreground italic text-center py-2">
|
||||||
|
No students match “{studentQuery}”.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
{students.length === 0 && <p className="text-xs text-muted-foreground italic text-center py-2">No students available</p>}
|
{students.length === 0 && <p className="text-xs text-muted-foreground italic text-center py-2">No students available</p>}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user