diff --git a/bunkialo-landing/src/app/api/bunkx/session/[sid]/route.ts b/bunkialo-landing/src/app/api/bunkx/session/[sid]/route.ts
deleted file mode 100644
index 7b0581e..0000000
--- a/bunkialo-landing/src/app/api/bunkx/session/[sid]/route.ts
+++ /dev/null
@@ -1,31 +0,0 @@
-import { consumeSession } from "@/lib/bunkx-session-store";
-import { NextResponse } from "next/server";
-
-export async function GET(
- _request: Request,
- context: { params: Promise<{ sid: string }> },
-) {
- const params = await context.params;
- const payload = consumeSession(params.sid);
-
- if (!payload) {
- return NextResponse.json(
- {
- error: "Session not found or expired",
- },
- {
- status: 404,
- headers: {
- "Cache-Control": "no-store",
- },
- },
- );
- }
-
- return NextResponse.json(payload, {
- status: 200,
- headers: {
- "Cache-Control": "no-store",
- },
- });
-}
diff --git a/bunkialo-landing/src/app/api/bunkx/session/route.ts b/bunkialo-landing/src/app/api/bunkx/session/route.ts
deleted file mode 100644
index 2190f5c..0000000
--- a/bunkialo-landing/src/app/api/bunkx/session/route.ts
+++ /dev/null
@@ -1,58 +0,0 @@
-import { parseBunkxAttendancePayload } from "@/lib/bunkx-payload";
-import { createSession } from "@/lib/bunkx-session-store";
-import { NextResponse } from "next/server";
-
-const toSafeErrorMessage = (error: unknown): string => {
- if (!(error instanceof Error)) {
- return "Invalid request payload";
- }
-
- const knownValidationMessages = new Set([
- "Payload must be an object",
- "attendance_rows must be an array",
- "Invalid attendance row",
- "Invalid period_date",
- "Invalid session_time",
- "Invalid course_code",
- "Invalid subject_name",
- "Invalid faculty",
- "Invalid faculty_email",
- "Invalid score",
- ]);
-
- if (knownValidationMessages.has(error.message)) {
- return error.message;
- }
-
- return "An unexpected error occurred";
-};
-
-export async function POST(request: Request) {
- try {
- const payload = parseBunkxAttendancePayload(
- (await request.json()) as unknown,
- );
- const session = createSession(payload);
-
- return NextResponse.json(session, {
- status: 201,
- headers: {
- "Cache-Control": "no-store",
- },
- });
- } catch (error) {
- const message = toSafeErrorMessage(error);
-
- return NextResponse.json(
- {
- error: message,
- },
- {
- status: 400,
- headers: {
- "Cache-Control": "no-store",
- },
- },
- );
- }
-}
diff --git a/bunkialo-landing/src/app/bunkialo/page.tsx b/bunkialo-landing/src/app/bunkialo/page.tsx
deleted file mode 100644
index 9e11a89..0000000
--- a/bunkialo-landing/src/app/bunkialo/page.tsx
+++ /dev/null
@@ -1,85 +0,0 @@
-import type { BunkxAttendancePayload } from "@/lib/bunkx-payload";
-import { headers } from "next/headers";
-
-interface BunkialoPageProps {
- searchParams: Promise<{ sid?: string }>;
-}
-
-export default async function BunkialoPage({
- searchParams,
-}: BunkialoPageProps) {
- const params = await searchParams;
- const sid = params.sid?.trim();
-
- if (!sid) {
- return (
-
- Missing session id
-
- Open this page from the app so attendance can be transferred securely.
-
-
- );
- }
-
- const requestHeaders = await headers();
- const host =
- requestHeaders.get("x-forwarded-host") ?? requestHeaders.get("host");
- const proto = requestHeaders.get("x-forwarded-proto") ?? "https";
- const fallbackHost = process.env.DEFAULT_HOST?.trim();
-
- if (!host && !fallbackHost) {
- return (
-
- Session unavailable
-
- Unable to resolve server host. Please try again in a moment.
-
-
- );
- }
-
- const resolvedHost = host ?? fallbackHost!;
- const apiUrl = `${proto}://${resolvedHost}/api/bunkx/session/${encodeURIComponent(sid)}`;
-
- const response = await fetch(apiUrl, {
- cache: "no-store",
- });
-
- let payload: BunkxAttendancePayload | null = null;
- if (response.ok) {
- try {
- payload = (await response.json()) as BunkxAttendancePayload;
- } catch {
- payload = null;
- }
- }
-
- if (!payload) {
- return (
-
- Session unavailable
-
- This session has expired or was already used. Please launch Bunkx
- again from the app.
-
-
- );
- }
-
- const attendanceCount = Array.isArray(payload.attendance_rows)
- ? payload.attendance_rows.length
- : 0;
-
- return (
-
- Bunkialo attendance handoff
-
- Received {attendanceCount} records from the mobile app.
-
-
- {JSON.stringify(payload, null, 2)}
-
-
- );
-}
diff --git a/bunkialo-landing/src/lib/bunkx-payload.ts b/bunkialo-landing/src/lib/bunkx-payload.ts
deleted file mode 100644
index f9048fd..0000000
--- a/bunkialo-landing/src/lib/bunkx-payload.ts
+++ /dev/null
@@ -1,68 +0,0 @@
-export interface BunkxAttendanceRow {
- period_date: string;
- session_time: string;
- course_code: string;
- subject_name: string;
- faculty: string;
- faculty_email: string;
- course?: string;
- score: string;
- record_id?: string;
-}
-
-export interface BunkxAttendancePayload {
- attendance_rows: BunkxAttendanceRow[];
- dataset_id?: string;
- dataset_expires_at?: string;
-}
-
-const parseString = (value: unknown, field: string): string => {
- if (typeof value !== "string") {
- throw new Error(`Invalid ${field}`);
- }
- return value;
-};
-
-const parseOptionalString = (value: unknown): string | undefined => {
- return typeof value === "string" ? value : undefined;
-};
-
-const isRecord = (value: unknown): value is Record =>
- typeof value === "object" && value !== null;
-
-const parseRow = (value: unknown): BunkxAttendanceRow => {
- if (!isRecord(value)) {
- throw new Error("Invalid attendance row");
- }
-
- return {
- period_date: parseString(value.period_date, "period_date"),
- session_time: parseString(value.session_time, "session_time"),
- course_code: parseString(value.course_code, "course_code"),
- subject_name: parseString(value.subject_name, "subject_name"),
- faculty: parseString(value.faculty, "faculty"),
- faculty_email: parseString(value.faculty_email, "faculty_email"),
- course: parseOptionalString(value.course),
- score: parseString(value.score, "score"),
- record_id: parseOptionalString(value.record_id),
- };
-};
-
-export const parseBunkxAttendancePayload = (
- value: unknown,
-): BunkxAttendancePayload => {
- if (!isRecord(value)) {
- throw new Error("Payload must be an object");
- }
-
- const rows = value.attendance_rows;
- if (!Array.isArray(rows)) {
- throw new Error("attendance_rows must be an array");
- }
-
- return {
- attendance_rows: rows.map(parseRow),
- dataset_id: parseOptionalString(value.dataset_id),
- dataset_expires_at: parseOptionalString(value.dataset_expires_at),
- };
-};
diff --git a/bunkialo-landing/src/lib/bunkx-session-store.ts b/bunkialo-landing/src/lib/bunkx-session-store.ts
deleted file mode 100644
index e9ca8fe..0000000
--- a/bunkialo-landing/src/lib/bunkx-session-store.ts
+++ /dev/null
@@ -1,73 +0,0 @@
-import type { BunkxAttendancePayload } from "@/lib/bunkx-payload";
-
-interface SessionRecord {
- payload: BunkxAttendancePayload;
- expiresAtMs: number;
-}
-
-interface CreatedSession {
- sid: string;
- expiresAt: string;
-}
-
-const DEFAULT_TTL_MS = 10 * 60 * 1000;
-const sessionStore = new Map();
-
-const cleanupExpiredSessions = (): void => {
- const now = Date.now();
- for (const [sid, record] of sessionStore.entries()) {
- if (record.expiresAtMs <= now) {
- sessionStore.delete(sid);
- }
- }
-};
-
-const generateSessionId = (): string => {
- return crypto.randomUUID();
-};
-
-const generateUniqueSessionId = (): string => {
- const maxAttempts = 8;
-
- for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
- const sid = generateSessionId();
- if (!sessionStore.has(sid)) {
- return sid;
- }
- }
-
- throw new Error("Could not generate unique session id");
-};
-
-export const createSession = (
- payload: BunkxAttendancePayload,
- ttlMs: number = DEFAULT_TTL_MS,
-): CreatedSession => {
- cleanupExpiredSessions();
-
- const now = Date.now();
- const expiresAtMs = now + Math.max(60_000, ttlMs);
- const sid = generateUniqueSessionId();
-
- sessionStore.set(sid, {
- payload,
- expiresAtMs,
- });
-
- return {
- sid,
- expiresAt: new Date(expiresAtMs).toISOString(),
- };
-};
-
-export const consumeSession = (sid: string): BunkxAttendancePayload | null => {
- cleanupExpiredSessions();
-
- const existing = sessionStore.get(sid);
- if (!existing) {
- return null;
- }
-
- sessionStore.delete(sid);
- return existing.payload;
-};
diff --git a/src/app/(tabs)/index.tsx b/src/app/(tabs)/index.tsx
index b8512e0..3421694 100644
--- a/src/app/(tabs)/index.tsx
+++ b/src/app/(tabs)/index.tsx
@@ -1,25 +1,22 @@
import { startBackgroundRefresh } from "@/background/dashboard-background";
-import { Toast } from "@/components";
import { EventCard } from "@/components/dashboard/event-card";
-import { NoticePopup } from "@/components/dashboard/notice-popup";
-import { NoticesModal } from "@/components/dashboard/notices-modal";
import { TimelineSection } from "@/components/dashboard/timeline-section";
import { UpNextSection } from "@/components/dashboard/up-next-section";
+import { NoticePopup } from "@/components/dashboard/notice-popup";
+import { NoticesModal } from "@/components/dashboard/notices-modal";
import { DevInfoModal } from "@/components/modals/dev-info-modal";
import { Container } from "@/components/ui/container";
-import { Colors } from "@/constants/theme";
import { POPUP_NOTICES } from "@/data/popups";
+import { Colors } from "@/constants/theme";
import { useColorScheme } from "@/hooks/use-color-scheme";
-import { createBunkxSession } from "@/services/bunkx-session";
-import { useAttendanceStore } from "@/stores/attendance-store";
import { useAuthStore } from "@/stores/auth-store";
+import { useAttendanceStore } from "@/stores/attendance-store";
import { useDashboardStore } from "@/stores/dashboard-store";
import { useLmsResourcesStore } from "@/stores/lms-resources-store";
-import { usePopupStore } from "@/stores/popup-store";
import { useSettingsStore } from "@/stores/settings-store";
-import { buildBunkxAttendancePayload } from "@/utils/bunkx-payload";
-import { initializeNotifications } from "@/utils/notifications";
+import { usePopupStore } from "@/stores/popup-store";
import { scheduleIdleTask } from "@/utils/scheduling";
+import { initializeNotifications } from "@/utils/notifications";
import { Ionicons } from "@expo/vector-icons";
import { useIsFocused } from "@react-navigation/native";
import { router, useFocusEffect } from "expo-router";
@@ -65,13 +62,11 @@ export default function DashboardScreen() {
hasHydrated,
} = useDashboardStore();
const fetchAttendance = useAttendanceStore((state) => state.fetchAttendance);
- const attendanceCourses = useAttendanceStore((state) => state.courses);
- const attendanceLastSyncTime = useAttendanceStore(
- (state) => state.lastSyncTime,
- );
const { isOffline, setOffline, username } = useAuthStore();
- const { hasHydrated: resourcesHydrated, prefetchEnrolledCourseResources } =
- useLmsResourcesStore();
+ const {
+ hasHydrated: resourcesHydrated,
+ prefetchEnrolledCourseResources,
+ } = useLmsResourcesStore();
const refreshIntervalMinutes = useSettingsStore(
(state) => state.refreshIntervalMinutes,
);
@@ -97,14 +92,11 @@ export default function DashboardScreen() {
isAttendanceRefreshQueued.current = true;
const interactionTask = InteractionManager.runAfterInteractions(() => {
- const cancelIdleTask = scheduleIdleTask(
- () => {
- void fetchAttendance({ background: true }).finally(() => {
- isAttendanceRefreshQueued.current = false;
- });
- },
- { timeoutMs: 1500, fallbackDelayMs: 120 },
- );
+ const cancelIdleTask = scheduleIdleTask(() => {
+ void fetchAttendance({ background: true }).finally(() => {
+ isAttendanceRefreshQueued.current = false;
+ });
+ }, { timeoutMs: 1500, fallbackDelayMs: 120 });
return cancelIdleTask;
});
@@ -206,9 +198,8 @@ export default function DashboardScreen() {
lastSyncTime !== null &&
Date.now() - lastSyncTime > staleAfterMs;
- let task: ReturnType<
- typeof InteractionManager.runAfterInteractions
- > | null = null;
+ let task: ReturnType | null =
+ null;
if (shouldRefreshOnFocus) {
task = InteractionManager.runAfterInteractions(() => {
@@ -248,25 +239,6 @@ export default function DashboardScreen() {
})();
}, [fetchDashboard, queueInvisibleAttendanceRefresh]);
- const handleOpenBunkx = useCallback(() => {
- setShowFabMenu(false);
-
- void (async () => {
- try {
- const payload = buildBunkxAttendancePayload(
- attendanceCourses,
- attendanceLastSyncTime,
- );
-
- const launchUrl = await createBunkxSession(payload);
- await Linking.openURL(launchUrl);
- } catch (error) {
- const errorMessage =
- error instanceof Error && error.message ? ` (${error.message})` : "";
- Toast.show(`Could not open Bunkx${errorMessage}`, { type: "error" });
- }
- })();
- }, [attendanceCourses, attendanceLastSyncTime]);
const hasOverdue = overdueEvents.length > 0;
const isEmpty = upcomingEvents.length === 0 && overdueEvents.length === 0;
const isHydratingFromCache = !hasHydrated && isEmpty;
@@ -283,10 +255,10 @@ export default function DashboardScreen() {
paddingHorizontal: 8,
paddingVertical: 4,
};
- const themeIconName = isDark ? "moon-outline" : "sunny-outline";
+ const themeIconName =
+ isDark ? "moon-outline" : "sunny-outline";
- const isOldBatch =
- username?.startsWith("2022") || username?.startsWith("2023");
+ const isOldBatch = username?.startsWith("2022") || username?.startsWith("2023");
const fabActions = [
{
@@ -313,82 +285,73 @@ export default function DashboardScreen() {
router.push("/gpa");
},
},
- {
- icon: "open-in-new",
- label: "Bunkx",
- color: theme.text,
- style: { backgroundColor: theme.backgroundSecondary },
- labelStyle: actionLabelStyle,
- containerStyle: actionContainerStyle,
- onPress: handleOpenBunkx,
- },
...(isOldBatch
? [
- {
- icon: "open-in-new",
- label: "Outpass-RFID",
- color: theme.text,
- style: { backgroundColor: theme.backgroundSecondary },
- labelStyle: actionLabelStyle,
- containerStyle: actionContainerStyle,
- onPress: () => {
- setShowFabMenu(false);
- Linking.openURL("https://outpass.iiitkottayam.ac.in/app");
- },
+ {
+ icon: "open-in-new",
+ label: "Outpass-RFID",
+ color: theme.text,
+ style: { backgroundColor: theme.backgroundSecondary },
+ labelStyle: actionLabelStyle,
+ containerStyle: actionContainerStyle,
+ onPress: () => {
+ setShowFabMenu(false);
+ Linking.openURL("https://outpass.iiitkottayam.ac.in/app");
},
- {
- icon: "open-in-new",
- label: "Outpass-fingerprint",
- color: theme.text,
- style: { backgroundColor: theme.backgroundSecondary },
- labelStyle: actionLabelStyle,
- containerStyle: actionContainerStyle,
- onPress: () => {
- setShowFabMenu(false);
- Linking.openURL(
- "https://gatepassstud.iiitkottayam.ac.in/index.php",
- );
- },
+ },
+ {
+ icon: "open-in-new",
+ label: "Outpass-fingerprint",
+ color: theme.text,
+ style: { backgroundColor: theme.backgroundSecondary },
+ labelStyle: actionLabelStyle,
+ containerStyle: actionContainerStyle,
+ onPress: () => {
+ setShowFabMenu(false);
+ Linking.openURL(
+ "https://gatepassstud.iiitkottayam.ac.in/index.php",
+ );
},
- {
- icon: "food",
- label: "Feaston",
- color: theme.text,
- style: { backgroundColor: theme.backgroundSecondary },
- labelStyle: actionLabelStyle,
- containerStyle: actionContainerStyle,
- onPress: () => {
- setShowFabMenu(false);
- Linking.openURL("https://feaston.iiitkottayam.ac.in/dashboard");
- },
+ },
+ {
+ icon: "food",
+ label: "Feaston",
+ color: theme.text,
+ style: { backgroundColor: theme.backgroundSecondary },
+ labelStyle: actionLabelStyle,
+ containerStyle: actionContainerStyle,
+ onPress: () => {
+ setShowFabMenu(false);
+ Linking.openURL("https://feaston.iiitkottayam.ac.in/dashboard");
},
- ]
+ },
+ ]
: [
- {
- icon: "open-in-new",
- label: "Outpass",
- color: theme.text,
- style: { backgroundColor: theme.backgroundSecondary },
- labelStyle: actionLabelStyle,
- containerStyle: actionContainerStyle,
- onPress: () => {
- setShowFabMenu(false);
- Linking.openURL("https://outpass.iiitkottayam.ac.in/app");
- },
+ {
+ icon: "open-in-new",
+ label: "Outpass",
+ color: theme.text,
+ style: { backgroundColor: theme.backgroundSecondary },
+ labelStyle: actionLabelStyle,
+ containerStyle: actionContainerStyle,
+ onPress: () => {
+ setShowFabMenu(false);
+ Linking.openURL("https://outpass.iiitkottayam.ac.in/app");
},
- {
- icon: "food",
- label: "Feaston",
- color: theme.text,
- style: { backgroundColor: theme.backgroundSecondary },
- labelStyle: actionLabelStyle,
- containerStyle: actionContainerStyle,
- onPress: () => {
- setShowFabMenu(false);
- Linking.openURL("https://feaston.iiitkottayam.ac.in/dashboard");
- },
+ },
+ {
+ icon: "food",
+ label: "Feaston",
+ color: theme.text,
+ style: { backgroundColor: theme.backgroundSecondary },
+ labelStyle: actionLabelStyle,
+ containerStyle: actionContainerStyle,
+ onPress: () => {
+ setShowFabMenu(false);
+ Linking.openURL("https://feaston.iiitkottayam.ac.in/dashboard");
},
- ]),
+ },
+ ]),
{
icon: "calendar-month",
label: "Academic Calendar",
@@ -418,17 +381,16 @@ export default function DashboardScreen() {
{/* Header */}
-
+
Dashboard
{lastSyncTime && (
{hasUnseenPopups && (
-
+
)}
- setShowDevInfo(true)} className="p-2">
+ setShowDevInfo(true)}
+ className="p-2"
+ >
- router.push("/settings")} className="p-2">
+ router.push("/settings")}
+ className="p-2"
+ >
-
- {overdueEvents.length} overdue task
- {overdueEvents.length > 1 ? "s" : ""}
+
+ {overdueEvents.length} overdue task{overdueEvents.length > 1 ? "s" : ""}
-
+
Upcoming
@@ -585,9 +543,9 @@ export default function DashboardScreen() {
onClose={() => setShowDevInfo(false)}
/>
- setShowNoticesModal(false)}
+ setShowNoticesModal(false)}
/>
diff --git a/src/services/bunkx-session.ts b/src/services/bunkx-session.ts
deleted file mode 100644
index c5bb54f..0000000
--- a/src/services/bunkx-session.ts
+++ /dev/null
@@ -1,64 +0,0 @@
-import type { BunkxAttendancePayload } from "@/types";
-
-const BUNKX_BASE_URL = "https://bunkx-iiitk.vercel.app";
-const SESSION_CREATE_ENDPOINT = `${BUNKX_BASE_URL}/api/bunkx/session`;
-
-interface SessionCreateResponse {
- sid: string;
- expiresAt?: string;
-}
-
-const parseSessionCreateResponse = (value: unknown): SessionCreateResponse => {
- if (!value || typeof value !== "object") {
- throw new Error("Invalid Bunkx session response");
- }
-
- const sidValue = (value as { sid?: unknown }).sid;
- const expiresAtValue = (value as { expiresAt?: unknown }).expiresAt;
-
- if (!sidValue || typeof sidValue !== "string") {
- throw new Error("Missing session id in Bunkx response");
- }
-
- return {
- sid: sidValue,
- expiresAt: typeof expiresAtValue === "string" ? expiresAtValue : undefined,
- };
-};
-
-export const createBunkxSession = async (
- payload: BunkxAttendancePayload,
-): Promise => {
- const abortController = new AbortController();
- const timeoutId = setTimeout(() => {
- abortController.abort();
- }, 15000);
-
- try {
- const response = await fetch(SESSION_CREATE_ENDPOINT, {
- method: "POST",
- headers: {
- "Content-Type": "application/json",
- },
- body: JSON.stringify(payload),
- signal: abortController.signal,
- });
-
- if (!response.ok) {
- throw new Error(`Session creation failed (${response.status})`);
- }
-
- const parsed = parseSessionCreateResponse(
- (await response.json()) as unknown,
- );
-
- return `${BUNKX_BASE_URL}/bunkialo?sid=${encodeURIComponent(parsed.sid)}`;
- } catch (error) {
- if (error instanceof Error && error.name === "AbortError") {
- throw new Error("Session request timed out");
- }
- throw error;
- } finally {
- clearTimeout(timeoutId);
- }
-};
diff --git a/src/types/attendance.ts b/src/types/attendance.ts
index 472862b..c4b9a2c 100644
--- a/src/types/attendance.ts
+++ b/src/types/attendance.ts
@@ -58,21 +58,3 @@ export interface CourseStats {
totalAttended: number;
overallPercentage: number;
}
-
-export interface BunkxAttendanceRow {
- period_date: string;
- session_time: string;
- course_code: string;
- subject_name: string;
- faculty: string;
- faculty_email: string;
- course?: string;
- score: string;
- record_id?: string;
-}
-
-export interface BunkxAttendancePayload {
- attendance_rows: BunkxAttendanceRow[];
- dataset_id?: string;
- dataset_expires_at?: string;
-}
diff --git a/src/utils/bunkx-payload.ts b/src/utils/bunkx-payload.ts
deleted file mode 100644
index bacc638..0000000
--- a/src/utils/bunkx-payload.ts
+++ /dev/null
@@ -1,188 +0,0 @@
-import type {
- BunkxAttendancePayload,
- BunkxAttendanceRow,
- CourseAttendance,
-} from "@/types";
-import { extractCourseCode, extractCourseName } from "@/utils/course-name";
-
-const MONTHS: Record = {
- jan: 0,
- feb: 1,
- mar: 2,
- apr: 3,
- may: 4,
- jun: 5,
- jul: 6,
- aug: 7,
- sep: 8,
- oct: 9,
- nov: 10,
- dec: 11,
-};
-
-const pad2 = (value: number): string => (value < 10 ? `0${value}` : `${value}`);
-
-const toIsoDate = (date: Date): string => {
- const year = date.getFullYear();
- const month = pad2(date.getMonth() + 1);
- const day = pad2(date.getDate());
- return `${year}-${month}-${day}`;
-};
-
-const parsePeriodDate = (rawDate: string, fallbackMs: number): string => {
- const dateMatch = rawDate.match(/(\d{1,2})\s+([A-Za-z]{3})\s+(\d{4})/);
- if (!dateMatch) {
- return toIsoDate(new Date(fallbackMs));
- }
-
- const day = Number(dateMatch[1]);
- const month = MONTHS[dateMatch[2].toLowerCase()];
- const year = Number(dateMatch[3]);
- if (month === undefined) {
- return toIsoDate(new Date(fallbackMs));
- }
-
- const parsed = new Date(year, month, day);
- if (isNaN(parsed.getTime())) {
- return toIsoDate(new Date(fallbackMs));
- }
-
- if (
- parsed.getFullYear() !== year ||
- parsed.getMonth() !== month ||
- parsed.getDate() !== day
- ) {
- return toIsoDate(new Date(fallbackMs));
- }
-
- return toIsoDate(parsed);
-};
-
-const parseSessionTime = (rawDate: string): string => {
- const timeMatch = rawDate.match(
- /(\d{1,2}(?::\d{2})?\s*(?:AM|PM))\s*-\s*(\d{1,2}(?::\d{2})?\s*(?:AM|PM))/i,
- );
- if (!timeMatch) {
- return "";
- }
-
- const start = timeMatch[1].replace(/\s+/g, " ").trim().toUpperCase();
- const end = timeMatch[2].replace(/\s+/g, " ").trim().toUpperCase();
- return `${start} - ${end}`;
-};
-
-const parseFacultyDetails = (
- description: string,
- remarks?: string,
-): { faculty: string; faculty_email: string } => {
- const source = `${description} ${remarks ?? ""}`.trim();
-
- const facultyLabelMatch = source.match(
- /(?:faculty|teacher|staff|by)\s*[:\-]\s*([^,;|]+)/i,
- );
- const facultyFromLabel = facultyLabelMatch?.[1]?.trim();
-
- return {
- faculty: facultyFromLabel || "Unknown",
- faculty_email: "",
- };
-};
-
-const normalizeScore = (points: string, status: string): string => {
- const compact = points.replace(/\s+/g, "").trim();
- if (compact) {
- if (compact.indexOf("?") !== -1) return "?/1";
- return compact;
- }
-
- if (status === "Present") return "1/1";
- if (status === "Unknown") return "?/1";
- return "0/1";
-};
-
-const toAttendanceRows = (
- courses: CourseAttendance[],
- fallbackMs: number,
-): BunkxAttendanceRow[] => {
- const rows: BunkxAttendanceRow[] = [];
-
- courses.forEach((course: CourseAttendance) => {
- const course_code = extractCourseCode(course.courseName);
- const subject_name = extractCourseName(course.courseName);
- const mergedCourseName = `${course_code} ${subject_name}`.trim();
-
- course.records.forEach((record, recordIndex) => {
- const { faculty, faculty_email } = parseFacultyDetails(
- record.description,
- record.remarks,
- );
-
- rows.push({
- period_date: parsePeriodDate(record.date, fallbackMs),
- session_time: parseSessionTime(record.date),
- course_code,
- subject_name,
- faculty,
- faculty_email,
- course: mergedCourseName,
- score: normalizeScore(record.points, record.status),
- record_id: `${course.courseId}-${recordIndex + 1}`,
- });
- });
- });
- return rows;
-};
-
-export const buildBunkxAttendancePayload = (
- courses: CourseAttendance[],
- lastSyncTime: number | null,
-): BunkxAttendancePayload => {
- const nowMs = Date.now();
- const referenceMs = Math.max(nowMs, lastSyncTime ?? nowMs);
- const expiresAtMs = referenceMs + 30 * 60 * 1000;
-
- return {
- attendance_rows: toAttendanceRows(courses, referenceMs),
- dataset_id: `bunkialo-${referenceMs}`,
- dataset_expires_at: new Date(expiresAtMs).toISOString(),
- };
-};
-
-const encodeBytesToBase64 = (bytes: number[]): string => {
- const base64Chars =
- "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
- let encoded = "";
-
- for (let i = 0; i < bytes.length; i += 3) {
- const chunk =
- (bytes[i] << 16) | ((bytes[i + 1] ?? 0) << 8) | (bytes[i + 2] ?? 0);
-
- encoded += base64Chars[(chunk >> 18) & 63];
- encoded += base64Chars[(chunk >> 12) & 63];
- encoded += i + 1 < bytes.length ? base64Chars[(chunk >> 6) & 63] : "=";
- encoded += i + 2 < bytes.length ? base64Chars[chunk & 63] : "=";
- }
-
- return encoded;
-};
-
-export const encodeBunkxPayload = (payload: BunkxAttendancePayload): string => {
- const json = JSON.stringify(payload);
- const uriEncoded = encodeURIComponent(json);
- const bytes: number[] = [];
-
- let index = 0;
- while (index < uriEncoded.length) {
- const char = uriEncoded.charAt(index);
- if (char === "%" && index + 2 < uriEncoded.length) {
- bytes.push(parseInt(uriEncoded.slice(index + 1, index + 3), 16));
- index += 3;
- continue;
- }
-
- bytes.push(uriEncoded.charCodeAt(index));
- index += 1;
- }
-
- return encodeBytesToBase64(bytes);
-};