fix teacher, parent and admin pages
This commit is contained in:
@@ -0,0 +1,368 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Link, useParams } from 'react-router-dom'
|
||||
import {
|
||||
fetchTeacherDashboard,
|
||||
isTeacherDashboardApiEnabled,
|
||||
type TeacherDashboardPayload,
|
||||
} from '../../api/teacherSession'
|
||||
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[] }
|
||||
|
||||
/** 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 { data, loading, error } = useTeacherResource<{
|
||||
invalidChoice?: string
|
||||
fallPath?: string
|
||||
springPath?: string
|
||||
}>('/select-semester')
|
||||
|
||||
const fallTo = data?.fallPath ?? '/app/teacher/scores?semester=Fall'
|
||||
const springTo = data?.springPath ?? '/app/teacher/scores?semester=Spring'
|
||||
|
||||
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> "{data.invalidChoice}" 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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user