feat(parent): add attention banner, export button, and grade detail
- Add parent-attention-banner for highlighting items needing attention - Add parent-export-button for data export capability - Add child-grade-detail component for detailed grade viewing - Update existing child-card, child-detail-header, child-grade-summary, child-schedule-card - Update parent-children-data-page and data-access
This commit is contained in:
@@ -3,22 +3,97 @@ import { CalendarDays, CalendarX } from "lucide-react"
|
||||
import { ScheduleList } from "@/shared/components/schedule/schedule-list"
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/shared/components/ui/card"
|
||||
import { EmptyState } from "@/shared/components/ui/empty-state"
|
||||
import type { ChildScheduleItem } from "@/modules/parent/types"
|
||||
import { cn } from "@/shared/lib/utils"
|
||||
import type { ChildScheduleItem, ChildWeeklyScheduleItem } from "@/modules/parent/types"
|
||||
|
||||
const WEEKDAY_LABELS = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"] as const
|
||||
|
||||
const toWeekdayIndex = (w: 1 | 2 | 3 | 4 | 5 | 6 | 7): 0 | 1 | 2 | 3 | 4 | 5 | 6 =>
|
||||
(w - 1) as 0 | 1 | 2 | 3 | 4 | 5 | 6
|
||||
|
||||
const isToday = (w: 1 | 2 | 3 | 4 | 5 | 6 | 7): boolean => {
|
||||
const today = new Date().getDay()
|
||||
const normalizedToday = today === 0 ? 7 : today
|
||||
return w === normalizedToday
|
||||
}
|
||||
|
||||
export function ChildScheduleCard({
|
||||
items,
|
||||
childName,
|
||||
weeklyItems,
|
||||
}: {
|
||||
items: ChildScheduleItem[]
|
||||
childName: string
|
||||
/** 完整周课表(可选)。提供时切换为周课表视图。 */
|
||||
weeklyItems?: ChildWeeklyScheduleItem[]
|
||||
}) {
|
||||
const hasSchedule = items.length > 0
|
||||
const hasWeekly = weeklyItems && weeklyItems.length > 0
|
||||
|
||||
if (hasWeekly) {
|
||||
const grouped: Record<number, ChildWeeklyScheduleItem[]> = {}
|
||||
for (const item of weeklyItems!) {
|
||||
const key = item.weekday
|
||||
if (!grouped[key]) grouped[key] = []
|
||||
grouped[key].push(item)
|
||||
}
|
||||
const weekdays = Object.keys(grouped).map(Number).sort((a, b) => a - b) as Array<1 | 2 | 3 | 4 | 5 | 6 | 7>
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2 text-base">
|
||||
<CalendarDays className="h-4 w-4 text-muted-foreground" aria-hidden />
|
||||
{childName}'s Weekly Schedule
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
{weekdays.map((w) => {
|
||||
const dayItems = grouped[w]
|
||||
const today = isToday(w)
|
||||
const label = WEEKDAY_LABELS[toWeekdayIndex(w)]
|
||||
return (
|
||||
<div
|
||||
key={w}
|
||||
className={cn(
|
||||
"rounded-md border p-3",
|
||||
today && "border-primary/40 bg-primary/5",
|
||||
)}
|
||||
>
|
||||
<div className="mb-2 flex items-center gap-2">
|
||||
<span className="text-sm font-medium">{label}</span>
|
||||
{today ? (
|
||||
<span className="rounded-full bg-primary/15 px-2 py-0.5 text-[10px] font-medium text-primary">
|
||||
Today
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
<ScheduleList
|
||||
items={dayItems.map((d) => ({
|
||||
id: d.id,
|
||||
classId: d.classId,
|
||||
className: d.className,
|
||||
course: d.course,
|
||||
startTime: d.startTime,
|
||||
endTime: d.endTime,
|
||||
location: d.location ?? null,
|
||||
}))}
|
||||
variant="separator"
|
||||
spacingClassName="space-y-2"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2 text-base">
|
||||
<CalendarDays className="h-4 w-4 text-muted-foreground" />
|
||||
<CalendarDays className="h-4 w-4 text-muted-foreground" aria-hidden />
|
||||
{childName}'s Today Schedule
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
|
||||
Reference in New Issue
Block a user