Files
alrahma_web_client/src/pages/teacher/TeacherCorePages.tsx
T
2026-06-07 00:52:04 -04:00

386 lines
13 KiB
TypeScript

import { useEffect, useState } from 'react'
import { Link, useParams, useSearchParams } from 'react-router-dom'
import {
fetchTeacherDashboard,
isTeacherDashboardApiEnabled,
type TeacherDashboardPayload,
} from '../../api/teacherSession'
import { useSchoolYearOptions } from '../../hooks/useSchoolYearOptions'
import { TeacherShell } from './TeacherShell'
import { useTeacherResource } from './useTeacherResource'
type TrashRow = { id: number; subject?: string; created_at?: string }
type TrashPayload = { trashedMessages?: TrashRow[] }
type MsgRow = { id: number; sender_id?: string | number; subject?: string; created_at?: string }
type InboxPayload = { receivedMessages?: MsgRow[] }
type SentPayload = { sentMessages?: MsgRow[] }
type TeacherMessagePayload = { message?: Record<string, unknown>; thread?: unknown[] }
function appendSchoolYearParam(path: string, schoolYear: string): string {
if (!schoolYear) return path
const [base, hash = ''] = path.split('#', 2)
const [pathname, query = ''] = base.split('?', 2)
const params = new URLSearchParams(query)
if (!params.get('school_year')) params.set('school_year', schoolYear)
const next = `${pathname}?${params.toString()}`
return hash ? `${next}#${hash}` : next
}
/** Every `/app/teacher/*` route so nothing is hidden behind the slim top nav alone. */
const TEACHER_PAGE_GROUPS = [
{
heading: 'Overview',
links: [
{ label: 'Dashboard', to: '/app/teacher_dashboard' },
{ label: 'No classes (notice)', to: '/app/teacher/no-classes' },
{ label: 'Select semester', to: '/app/teacher/select-semester' },
],
},
{
heading: 'Classes & attendance',
links: [
{ label: 'Class list', to: '/app/teacher/class-view' },
{ label: 'Attendance', to: '/app/teacher/showupdate-attendance' },
{ label: 'Absence / vacation', to: '/app/teacher/absence-vacation' },
],
},
{
heading: 'Grades & coursework',
links: [
{ label: 'Scores', to: '/app/teacher/scores' },
{ label: 'Homework list', to: '/app/teacher/homework-list' },
{ label: 'Add homework', to: '/app/teacher/add-homework' },
{ label: 'Add quiz', to: '/app/teacher/add-quiz' },
{ label: 'Add quiz column', to: '/app/teacher/add-quiz-column-form' },
{ label: 'Add midterm exam', to: '/app/teacher/add-midterm-exam' },
{ label: 'Add final exam', to: '/app/teacher/add-final-exam' },
{ label: 'Add participation', to: '/app/teacher/add-participation' },
{ label: 'Add project', to: '/app/teacher/add-project' },
{ label: 'Teacher assignments', to: '/app/teacher/teacher-assignment' },
{ label: 'Exam drafts', to: '/app/teacher/exam-drafts' },
],
},
{
heading: 'Class progress',
links: [
{ label: 'Submit progress', to: '/app/teacher/class-progress-submit' },
{ label: 'Progress history', to: '/app/teacher/class-progress-history' },
{ label: 'View progress', to: '/app/teacher/class-progress-view' },
],
},
{
heading: 'Competitions',
links: [
{
label: 'Competition scores',
to: '/app/teacher/competition-scores',
hint: 'Score entry uses /app/teacher/competition-scores/:competitionId',
},
],
},
{
heading: 'Calendar',
links: [{ label: 'Calendar', to: '/app/teacher/calendar' }],
},
{
heading: 'Messages',
links: [
{ label: 'Inbox', to: '/app/teacher/inbox' },
{ label: 'Sent', to: '/app/teacher/sent' },
{ label: 'Drafts', to: '/app/teacher/drafts' },
{ label: 'Trash', to: '/app/teacher/trash' },
{
label: 'Message thread',
to: '/app/teacher/inbox',
hint: '/app/teacher/messages/:messageId — open “View” from inbox.',
},
],
},
{
heading: 'Help',
links: [
{ label: 'Contact us', to: '/app/teacher/teacher-contactus' },
{ label: 'Support', to: '/app/teacher/teacher-support' },
],
},
] as const
/** `landing_page/teacher_dashboard` — optional `GET /api/v1/teacher/dashboard` (JWT) plus full route index. */
export function TeacherDashboardPage() {
const apiEnabled = isTeacherDashboardApiEnabled()
const [dash, setDash] = useState<TeacherDashboardPayload | null>(null)
const [dashLoading, setDashLoading] = useState(apiEnabled)
useEffect(() => {
if (!apiEnabled) return
let cancelled = false
fetchTeacherDashboard()
.then((d) => {
if (!cancelled) setDash(d)
})
.catch(() => {
if (!cancelled) setDash(null)
})
.finally(() => {
if (!cancelled) setDashLoading(false)
})
return () => {
cancelled = true
}
}, [apiEnabled])
const termLine =
dash?.school_year || dash?.semester
? [dash.school_year, dash.semester].filter(Boolean).join(' · ')
: null
const defaultSubtitle =
'All teacher portal screens are listed below. The top bar shows shortcuts only.'
return (
<TeacherShell
title="Teacher Dashboard"
subtitle={defaultSubtitle}
flash={dash?.welcome?.trim() ? String(dash.welcome) : undefined}
>
{dashLoading ? <p className="text-muted small mb-3">Loading dashboard</p> : null}
{termLine ? <p className="text-muted small mb-2">{termLine}</p> : null}
{dash?.counts && Object.keys(dash.counts).length > 0 ? (
<div className="row g-2 mb-4">
{Object.entries(dash.counts).map(([k, v]) => (
<div key={k} className="col-6 col-md-4 col-lg-2">
<div className="border rounded p-2 text-center small">
<div className="text-muted text-uppercase" style={{ fontSize: '0.7rem' }}>
{k.replace(/_/g, ' ')}
</div>
<div className="fw-semibold">{v != null && v !== '' ? String(v) : '—'}</div>
</div>
</div>
))}
</div>
) : null}
{dash?.announcements && dash.announcements.length > 0 ? (
<div className="mb-4">
{dash.announcements.map((a, i) => (
<div key={i} className="alert alert-light border mb-2">
{a.title ? <div className="fw-semibold">{a.title}</div> : null}
{a.body ? <div className="small mt-1">{a.body}</div> : null}
</div>
))}
</div>
) : null}
<p className="small text-muted mb-4">
CI view browser:{' '}
<Link to="/app/browse">/app/browse</Link> (registry links now map competition score views to this area).
</p>
{TEACHER_PAGE_GROUPS.map((group) => (
<section key={group.heading} className="mb-4" id={`teacher-${group.heading.replace(/\s+/g, '-').toLowerCase()}`}>
<h2 className="h6 text-uppercase text-muted mb-2">{group.heading}</h2>
<div className="row g-2">
{group.links.map((item) => (
<div className="col-6 col-md-4 col-lg-3" key={`${item.to}-${item.label}`}>
<Link className="btn btn-outline-primary w-100" to={item.to}>
{item.label}
</Link>
{'hint' in item && item.hint ? (
<p className="small text-muted mt-1 mb-0">{item.hint}</p>
) : null}
</div>
))}
</div>
</section>
))}
</TeacherShell>
)
}
/** `teacher/no_classes.php` */
export function TeacherNoClassesPage() {
const { data, loading, error } = useTeacherResource<{ message?: string }>('/no-classes')
const msg = data?.message
return (
<TeacherShell title="Classes" loading={loading} error={error} flash={msg}>
<p className="text-muted mb-0">If you believe this is an error, contact the school office.</p>
</TeacherShell>
)
}
/** `teacher/select_semester.php` */
export function TeacherSelectSemesterPage() {
const [searchParams] = useSearchParams()
const { data, loading, error } = useTeacherResource<{
invalidChoice?: string
fallPath?: string
springPath?: string
}>('/select-semester')
const requestedSchoolYear = searchParams.get('school_year')?.trim() ?? ''
const { selectedYear } = useSchoolYearOptions({
preferredYear: requestedSchoolYear,
})
const activeSchoolYear = requestedSchoolYear || selectedYear
const fallTo = appendSchoolYearParam(data?.fallPath ?? '/app/teacher/scores?semester=Fall', activeSchoolYear)
const springTo = appendSchoolYearParam(data?.springPath ?? '/app/teacher/scores?semester=Spring', activeSchoolYear)
return (
<TeacherShell title="Choose a semester" loading={loading} error={error}>
<div className="row justify-content-center">
<div className="col-md-8 col-lg-6">
<div className="card shadow-sm border-0">
<div className="card-body text-center">
<p className="text-muted mb-4">
Before viewing scores, pick the semester you want to work with.
</p>
{data?.invalidChoice ? (
<div className="alert alert-warning">
<strong>Note:</strong> &quot;{data.invalidChoice}&quot; is not a valid semester selection.
</div>
) : null}
<div className="d-flex flex-wrap justify-content-center gap-3">
<Link to={fallTo} className="btn btn-primary btn-lg px-4">
Fall
</Link>
<Link to={springTo} className="btn btn-outline-primary btn-lg px-4">
Spring
</Link>
</div>
</div>
</div>
</div>
</div>
</TeacherShell>
)
}
/** `teacher/trash.php` */
export function TeacherTrashPage() {
const { data, loading, error } = useTeacherResource<TrashPayload>('/trash')
const rows = data?.trashedMessages ?? []
return (
<TeacherShell title="Trash" loading={loading} error={error}>
{rows.length === 0 ? (
<p>No messages found in trash.</p>
) : (
<div className="table-responsive">
<table className="table table-striped">
<thead>
<tr>
<th>Subject</th>
<th>Date</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{rows.map((m) => (
<tr key={m.id}>
<td>{m.subject ?? '—'}</td>
<td>{m.created_at ?? ''}</td>
<td>
<span className="text-muted small">Restore / delete via API when wired.</span>
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</TeacherShell>
)
}
/** `teacher/inbox.php` */
export function TeacherInboxPage() {
const { data, loading, error } = useTeacherResource<InboxPayload>('/inbox')
const rows = data?.receivedMessages ?? []
return (
<TeacherShell title="Inbox" loading={loading} error={error}>
{rows.length === 0 ? (
<p>No messages found.</p>
) : (
<div className="table-responsive">
<table className="table table-striped">
<thead>
<tr>
<th>From</th>
<th>Subject</th>
<th>Date</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{rows.map((m) => (
<tr key={m.id}>
<td>{String(m.sender_id ?? '')}</td>
<td>{m.subject ?? ''}</td>
<td>{m.created_at ?? ''}</td>
<td>
<Link to={`/app/teacher/messages/${m.id}`} className="me-2">
View
</Link>
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</TeacherShell>
)
}
/** `teacher/sent.php` */
export function TeacherSentPage() {
const { data, loading, error } = useTeacherResource<SentPayload>('/sent')
const rows = data?.sentMessages ?? []
return (
<TeacherShell title="Sent" loading={loading} error={error}>
{rows.length === 0 ? (
<p>No sent messages.</p>
) : (
<div className="table-responsive">
<table className="table table-striped">
<thead>
<tr>
<th>To</th>
<th>Subject</th>
<th>Date</th>
</tr>
</thead>
<tbody>
{rows.map((m) => (
<tr key={m.id}>
<td>{String(m.sender_id ?? '')}</td>
<td>{m.subject ?? ''}</td>
<td>{m.created_at ?? ''}</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</TeacherShell>
)
}
/** `teacher/teacher_message.php` — thread payload from `/api/v1/teacher/messages/:id`. */
export function TeacherMessagePage() {
const { messageId } = useParams()
const path = messageId ? `/messages/${messageId}` : '/message'
const { data, loading, error } = useTeacherResource<TeacherMessagePayload>(path)
return (
<TeacherShell title="Message" loading={loading} error={error}>
<pre className="small bg-light p-3 rounded overflow-auto" style={{ maxHeight: 420 }}>
{data ? JSON.stringify(data, null, 2) : null}
</pre>
</TeacherShell>
)
}