-
Notifications
You must be signed in to change notification settings - Fork 0
Create a beautiful "Schedule Meetings" form #7
Description
The form shows and does the following:
Parent Selection - A dropdown at the top to select the parent.
Check box called "Include Students in Meeting" and "Inclued Spouse in Meeting". Default to last used value.
Student Rows with Teachers - Each of the parent's children is displayed in a row with their respective teachers shown as checkboxes.
Dynamic Teacher Columns - When teacher checkboxes are ticked, columns appear showing:
- The header of the column shows the teacher name and the students who have this teacher (with the related subjects in brackets)
- All time slots for that teacher listed vertically
- Color-Coded Time Slots:
-- Green background for available slots
-- Red background for booked slots
-- Blue slots for already booked slots for this parent. - Interactive Slot Contents:
-- For available slots: Buttons with students profile image or initials allowing you to book the slot for this parent. Clicking the student button will create a meeting. If the one teacher teaches both students, then a button for each student should be shown. The selected student(s) are added to the partner.meeting.connected_partner_ids list for the generated meeting. The rows at the very top that show students and teachers should be updated to indicate that the booking as been made for this student / teacher pair (change the background color to green for that teacher). This needs to be displayed for future access to this form also, not just when initially booking (look at the scheduled meetings, check the connected_partner_ids, highlight as needed). This helps the user know if a booking must still be made.
-- Add a note icon that when clicks pops up a dialog allowing the user to record notes for this booking.
-- Teachers icon showing the number of teachers/observers are in the meeting.
-- A People icon with a number after showing the number of parents/students in the meeting.
Always prompt if cancelling a meeting.
Other students not belonging to this parent who are already booked for a slot show in gray scale but with the red icon to cancel the booking. Include the other icons and numbers.
The interface supports multiple children per parent, allows booking multiple interviews simultaneously, and provides a visual overview of all time slot availability across selected teachers.
Timeslots should match across teachers. If one teacher is missing a slot then there should be a gap for that teacher. Teacher timeslots should scroll together so they are easily compared. Make the timeslot header sticky.
Children are determined from res.partner.relation.all model. A relation type with the name "Is Parent Of" or "Is Guardian Of" indicates a relationship we are interested in.
Teachers for the student are determined from the 'aps.class' and the 'aps.student.class' models. The class holds the teacher and assistants and the student class holds the student. Only include student classes that have status Enrolled.
meeting.connected_partner_ids is set to the student(s) who are the reason for the meeting.
Classes also have Assistant Teachers. When creating the meeting, assign members in this way:
- is_teacher for all teachers
- is_observer for all assistant teachers
- is_parent for parent(s). Always add the selected parent. Only include if the "Include Spouse in Meeting" is ticked.
- is_student for relevant students. Only include if the "Include Students in Meeting" is ticked.
Consult this document for detailed databse schematic: aps_sis\DATABASE_SCHEMA.md
Use this layout as a guide:
return (
<div className="space-y-6">
<div>
<h2 className="text-3xl font-semibold text-gray-900">Schedule Interviews</h2>
<p className="text-gray-600 mt-1">Book multiple interviews for a parent's children</p>
</div>
{/* Parent Selection */}
<Card>
<CardHeader>
<CardTitle>Select Parent</CardTitle>
<CardDescription>Choose the parent to schedule interviews for</CardDescription>
</CardHeader>
<CardContent>
<div className="w-full max-w-md">
<Label htmlFor="parent">Parent</Label>
<Select value={selectedParentId} onValueChange={setSelectedParentId}>
<SelectTrigger id="parent">
<SelectValue placeholder="Select a parent" />
</SelectTrigger>
<SelectContent>
{parents.map((parent) => (
<SelectItem key={parent.id} value={parent.id}>
{parent.name} ({parent.students.length} {parent.students.length === 1 ? 'child' : 'children'})
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</CardContent>
</Card>
{/* Students and Teachers Selection */}
{selectedParent && (
<Card>
<CardHeader>
<CardTitle>Students & Teachers</CardTitle>
<CardDescription>Select which teachers to schedule for each student</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-6">
{selectedParent.students.map((student) => (
<div key={student.id} className="border-b border-gray-200 pb-6 last:border-0">
<div className="flex items-start gap-8">
{/* Student Info */}
<div className="w-48 flex-shrink-0">
<div className="flex items-center gap-3">
<div className="w-12 h-12 rounded-full bg-blue-600 flex items-center justify-center text-white font-semibold">
{getInitials(student.name)}
</div>
<div>
<div className="font-medium text-gray-900">{student.name}</div>
<div className="text-sm text-gray-600">{student.grade}</div>
</div>
</div>
</div>
{/* Teachers Checkboxes */}
<div className="flex-1">
<div className="text-sm font-medium text-gray-700 mb-3">Teachers:</div>
<div className="grid grid-cols-2 gap-3">
{student.teacherIds.map((teacherId) => {
const teacher = teachers.find(t => t.id === teacherId);
if (!teacher) return null;
return (
<div key={teacherId} className="flex items-center space-x-2">
<Checkbox
id={`${student.id}-${teacherId}`}
checked={isTeacherSelected(student.id, teacherId)}
onCheckedChange={() => toggleTeacher(student.id, teacherId)}
/>
<label
htmlFor={`${student.id}-${teacherId}`}
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 cursor-pointer"
>
{teacher.name}
<span className="text-gray-500 ml-1">({teacher.subject})</span>
</label>
</div>
);
})}
</div>
</div>
</div>
</div>
))}
</div>
</CardContent>
</Card>
)}
{/* Time Slots Grid */}
{selectedParent && getSelectedTeachersForDisplay().length > 0 && (
<Card>
<CardHeader>
<CardTitle>Available Time Slots</CardTitle>
<CardDescription>
<div className="flex items-center gap-4 mt-2">
<div className="flex items-center gap-2">
<div className="w-4 h-4 bg-green-100 border border-green-300 rounded"></div>
<span className="text-sm">Available</span>
</div>
<div className="flex items-center gap-2">
<div className="w-4 h-4 bg-red-100 border border-red-300 rounded"></div>
<span className="text-sm">Booked</span>
</div>
</div>
</CardDescription>
</CardHeader>
<CardContent>
<div className="overflow-x-auto">
<div className="flex gap-4 min-w-max">
{getSelectedTeachersForDisplay().map((teacherId) => {
const teacher = teachers.find(t => t.id === teacherId);
if (!teacher) return null;
const teacherSlots = getTeacherTimeSlots(teacherId);
return (
<div key={teacherId} className="flex-shrink-0 w-64">
<div className="bg-gray-100 p-3 rounded-t-lg border border-gray-200">
<div className="font-semibold text-gray-900">{teacher.name}</div>
<div className="text-sm text-gray-600">{teacher.subject}</div>
</div>
<div className="space-y-2 p-2 border border-t-0 border-gray-200 rounded-b-lg bg-white">
{teacherSlots.map((slot) => {
const bookedStudents = getBookedStudentsForSlot(slot.id);
return (
<div
key={slot.id}
className={cn(
'border-2 rounded-lg p-3 transition-colors',
getSlotColor(slot.id)
)}
>
<div className="text-sm font-medium text-gray-900 mb-2">
{slot.startTime} - {slot.endTime}
</div>
{bookedStudents.length > 0 ? (
<div className="space-y-2">
{bookedStudents.map((bookedStudent) => (
<div
key={bookedStudent.id}
className="flex items-center gap-2 text-xs bg-white rounded p-2 border border-gray-200"
>
<div className="w-6 h-6 rounded-full bg-gray-600 flex items-center justify-center text-white text-xs">
{getInitials(bookedStudent.name)}
</div>
<span className="font-medium text-gray-700">{bookedStudent.name}</span>
</div>
))}
</div>
) : (
<div className="space-y-1">
{selectedParent.students
.filter(student => (selectedTeachers[student.id] || []).includes(teacherId))
.map((student) => {
const alreadyBooked = isStudentBookedForSlot(student.id, slot.id);
return (
<Button
key={student.id}
size="sm"
variant="outline"
disabled={alreadyBooked}
onClick={() => bookTimeSlot(student, teacherId, slot.id)}
className="w-full justify-start text-xs h-8 gap-2"
>
<div className="w-5 h-5 rounded-full bg-blue-600 flex items-center justify-center text-white text-xs">
{getInitials(student.name)}
</div>
{alreadyBooked ? `${student.name} (Booked)` : student.name}
</Button>
);
})}
</div>
)}
</div>
);
})}
</div>
</div>
);
})}
</div>
</div>
</CardContent>
</Card>
)}
{!selectedParent && (
<Card>
<CardContent className="py-12">
<div className="text-center text-gray-500">
Please select a parent to begin scheduling interviews
</div>
</CardContent>
</Card>
)}
</div>
);
}