diff --git a/src/app/lectureInformation/page.tsx b/src/app/lectureInformation/page.tsx new file mode 100644 index 0000000..bd6ac37 --- /dev/null +++ b/src/app/lectureInformation/page.tsx @@ -0,0 +1,256 @@ +import type { Metadata } from "next"; +import { api } from "@/lib/api"; + +export const metadata: Metadata = { + title: "休講・補講・教室変更", + description: "休講・補講・教室変更情報の一覧", +}; + +type LectureInfoType = "Cancelled" | "Makeup" | "RoomChanged"; + +type LectureInfoItem = { + id: string; + type: LectureInfoType; + date: string; + period: string; + subjectName: string; + detail: string; +}; + +function formatDate(iso: string): { year: string; month: string; day: string } { + const d = new Date(`${iso}T00:00:00`); + return { + year: String(d.getFullYear()), + month: String(d.getMonth() + 1).padStart(2, "0"), + day: String(d.getDate()).padStart(2, "0"), + }; +} + +function periodLabel(period: string): string { + return period.replace("Period", "") + "限"; +} + +function typeLabel(type: LectureInfoType): string { + if (type === "Cancelled") return "休講"; + if (type === "Makeup") return "補講"; + return "教室変更"; +} + +function typeBadgeClass(type: LectureInfoType): string { + if (type === "Cancelled") { + return "border-accent-error/30 text-accent-error bg-accent-error/5"; + } + if (type === "Makeup") { + return "border-accent-info/30 text-accent-info bg-accent-info/5"; + } + return "border-border-primary text-label-secondary bg-background-secondary"; +} + +function sortByDateDesc(a: LectureInfoItem, b: LectureInfoItem): number { + const aTime = new Date(`${a.date}T00:00:00`).getTime(); + const bTime = new Date(`${b.date}T00:00:00`).getTime(); + return bTime - aTime; +} + +function sectionLabel(type: LectureInfoType): string { + return typeLabel(type); +} + +function sectionId(type: LectureInfoType): string { + if (type === "Cancelled") return "cancelled"; + if (type === "Makeup") return "makeup"; + return "room-changed"; +} + +export default async function Page() { + const [cancelledRes, makeupRes, roomChangeRes] = await Promise.all([ + api.GET("/v1/cancelledClasses"), + api.GET("/v1/makeupClasses"), + api.GET("/v1/roomChanges"), + ]); + + const hasError = !!( + cancelledRes.error || + makeupRes.error || + roomChangeRes.error || + !cancelledRes.data || + !makeupRes.data || + !roomChangeRes.data + ); + + const cancelledItems: LectureInfoItem[] = hasError + ? [] + : cancelledRes.data.cancelledClasses + .map((item) => ({ + id: item.id, + type: "Cancelled" as const, + date: item.date, + period: item.period, + subjectName: item.subject.name, + detail: item.comment, + })) + .sort(sortByDateDesc); + + const makeupItems: LectureInfoItem[] = hasError + ? [] + : makeupRes.data.makeupClasses + .map((item) => ({ + id: item.id, + type: "Makeup" as const, + date: item.date, + period: item.period, + subjectName: item.subject.name, + detail: item.comment, + })) + .sort(sortByDateDesc); + + const roomChangedItems: LectureInfoItem[] = hasError + ? [] + : roomChangeRes.data.roomChanges + .map((item) => ({ + id: item.id, + type: "RoomChanged" as const, + date: item.date, + period: item.period, + subjectName: item.subject.name, + detail: `${item.originalRoom.name} → ${item.newRoom.name}`, + })) + .sort(sortByDateDesc); + + const sections: { type: LectureInfoType; items: LectureInfoItem[] }[] = [ + { type: "Cancelled", items: cancelledItems }, + { type: "Makeup", items: makeupItems }, + { type: "RoomChanged", items: roomChangedItems }, + ]; + + const totalCount = + cancelledItems.length + makeupItems.length + roomChangedItems.length; + + return ( +
+ {/* Hero Header */} +
+

+ Lecture / Information +

+
+

+ 休講・補講・教室変更 +

+ {!hasError && ( +

+ {totalCount} 件 +

+ )} +
+
+ + {hasError ? ( +
+

+ 情報の取得に失敗しました。 +

+
+ ) : totalCount === 0 ? ( +
+

現在、情報はありません

+
+ ) : ( +
+
+
+ {sections.map((section) => ( + +
+ + {sectionLabel(section.type)} + + + {section.items.length}件 + +
+
+ ))} +
+
+ + {sections.map((section) => ( +
+
+

+ {sectionLabel(section.type)} +

+ + {section.items.length}件 + +
+ + {section.items.length === 0 ? ( +
+

+ 該当する情報はありません +

+
+ ) : ( +
    + {section.items.map((item) => { + const { year, month, day } = formatDate(item.date); + + return ( +
  • +
    +
    + + {year} + + + {month} + / + {day} + +
    + + + +
    +
    + + {typeLabel(item.type)} + + + {periodLabel(item.period)} + +
    +

    + {item.subjectName} +

    +

    + {item.detail} +

    +
    +
    +
  • + ); + })} +
+ )} +
+ ))} +
+ )} +
+ ); +} diff --git a/src/components/layout/app-sidebar.tsx b/src/components/layout/app-sidebar.tsx index a7566a1..c593fc2 100644 --- a/src/components/layout/app-sidebar.tsx +++ b/src/components/layout/app-sidebar.tsx @@ -13,7 +13,7 @@ import { SidebarMenuButton, SidebarMenuItem, } from "@/components/ui/sidebar"; -import { BellIcon, HomeIcon, MonitorIcon } from "lucide-react"; +import { BellIcon, BookOpenIcon, HomeIcon, MonitorIcon } from "lucide-react"; import Image from "next/image"; import Link from "next/link"; import { usePathname } from "next/navigation"; @@ -22,6 +22,11 @@ import DottoIcon from "@/app/icon1024.png"; const navItems = [ { title: "ホーム", href: "/", icon: HomeIcon }, { title: "お知らせ", href: "/announcements", icon: BellIcon }, + { + title: "休講・補講・教室変更", + href: "/lectureInformation", + icon: BookOpenIcon, + }, { title: "Mac サポート", href: "/mac", icon: MonitorIcon }, ];