A web application for tracking student progress in ALEKS and managing a coin-based reward system.
git clone <repository-url>
cd aleks-coins-checker
npm install
npm run dev# Required
ADMIN_PASSWORD="your-secure-password"
# Optional (app works with demo data if not provided)
POSTGRES_URL="postgres://..."
NODE_ENV="development"- Framework: Next.js 14 (App Router)
- Language: TypeScript
- Styling: Tailwind CSS
- UI Components: Radix UI
- Database: PostgreSQL (Vercel Postgres)
- Charts: D3.js
- Excel Processing: XLSX library
- Icons: Lucide React
app/
βββ page.tsx # Student portal
βββ api/
β βββ student/
β β βββ route.ts # Student data lookup
β β βββ requests/route.ts # Student request submission
β βββ admin/
β β βββ auth/route.ts # Admin authentication
β β βββ upload/route.ts # Excel file upload
β β βββ requests/route.ts # Admin request management
β β βββ coin-adjustments/route.ts # Manual coin adjustments
β βββ analytics/route.ts # Class analytics
βββ admin/
βββ dashboard/page.tsx # Main admin interface
βββ requests/page.tsx # Request management UI
βββ coin-adjustments/page.tsx # Adjustment management UI
βββ view-data/page.tsx # Data viewing interface
components/
βββ redemption-modal.tsx # Coin redemption interface
βββ calendar-view.tsx # Daily progress calendar
βββ completion-chart.tsx # Analytics visualizations
lib/
βββ exam-periods.ts # Period configuration
βββ utils.ts # Utility functions
-- Student progress data (uploaded from Excel)
CREATE TABLE student_data (
id SERIAL PRIMARY KEY,
data JSONB,
period VARCHAR(100),
section_number VARCHAR(20),
uploaded_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
);
-- Student requests (redemptions + overrides)
CREATE TABLE student_requests (
id SERIAL PRIMARY KEY,
student_id VARCHAR(100),
student_name VARCHAR(255),
student_email VARCHAR(255),
period VARCHAR(100),
section_number VARCHAR(20),
request_type VARCHAR(50), -- assignment_replacement, quiz_replacement, override_request
request_details TEXT,
day_number INTEGER, -- For override requests
override_date VARCHAR(10), -- For override requests
submitted_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
status VARCHAR(50) DEFAULT 'pending',
admin_notes TEXT,
processed_at TIMESTAMPTZ,
processed_by VARCHAR(255)
);
-- Manual coin adjustments
CREATE TABLE coin_adjustments (
id SERIAL PRIMARY KEY,
student_id VARCHAR(100),
student_name VARCHAR(255),
period VARCHAR(100),
section_number VARCHAR(20),
adjustment_amount INTEGER,
reason TEXT,
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
created_by VARCHAR(255),
is_active BOOLEAN DEFAULT true,
request_id INTEGER REFERENCES student_requests(id) ON DELETE SET NULL -- Links to redemption request (nullable)
);
-- Per-student day overrides
CREATE TABLE student_day_overrides (
id SERIAL PRIMARY KEY,
student_id VARCHAR(100),
day_number INTEGER,
override_type VARCHAR(50),
reason TEXT,
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
);
-- Exam period configurations
CREATE TABLE exam_periods (
id SERIAL PRIMARY KEY,
period_key VARCHAR(100) UNIQUE,
period_name VARCHAR(255),
start_date DATE,
end_date DATE,
excluded_dates JSONB,
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
);CREATE INDEX idx_student_requests_student_id ON student_requests(student_id);
CREATE INDEX idx_student_requests_section ON student_requests(section_number, student_name);
CREATE INDEX idx_student_requests_type ON student_requests(request_type);
CREATE INDEX idx_coin_adjustments_student_id ON coin_adjustments(student_id);
CREATE INDEX idx_coin_adjustments_period ON coin_adjustments(period, section_number);// Get student data with all periods and adjustments
POST /api/student
Body: { studentId: string }
Returns: {
success: boolean
student: StudentInfo
periods: PeriodInfo[]
coinAdjustments: CoinAdjustment[]
totalCoinsAcrossPeriods: number
}
// Submit request (redemption or override)
POST /api/student/requests
Body: {
studentId: string
studentName: string
studentEmail: string
period: string
sectionNumber: string
requestType: 'assignment_replacement' | 'quiz_replacement' | 'override_request'
requestDetails: string
dayNumber?: number // Required for override_request
overrideDate?: string // Required for override_request
}
Returns: { success: boolean, requestId: number, submittedAt: string }// Authenticate admin
POST /api/admin/auth
Body: { password: string }
Returns: { success: boolean }
// Upload Excel file with student data
POST /api/admin/upload
Body: FormData {
file: File
password: string
examPeriod: string
sectionNumber: string
}
Returns: { success: boolean, studentCount: number }
// Get all student requests
GET /api/admin/requests?password={password}
Returns: { success: boolean, requests: StudentRequest[] }
// Update request status and optionally deduct coins
PUT /api/admin/requests
Body: {
password: string
requestId: number
status: 'pending' | 'approved' | 'rejected' | 'completed'
adminNotes?: string
coinDeduction?: number
}
Returns: { success: boolean, adjustmentId?: number }
// Get coin adjustments
GET /api/admin/coin-adjustments?studentId={id}
Returns: { success: boolean, adjustments: CoinAdjustment[] }
// Create coin adjustment
POST /api/admin/coin-adjustments
Body: {
password: string
studentId: string
studentName: string
period: string
sectionNumber: string
adjustmentAmount: number
reason: string
}
Returns: { success: boolean, adjustmentId: number }
// Delete (soft) coin adjustment
DELETE /api/admin/coin-adjustments
Body: { password: string, adjustmentId: number }
Returns: { success: boolean }// Per-period calculation
function calculatePeriodCoins(student: Student, period: Period): number {
// 1. Base coins from qualified days
const baseCoins = student.dailyLog
.filter(day => !day.isExcluded && day.qualified)
.length;
// 2. Exempt day credits (would have qualified on exempt days)
const exemptCredits = student.dailyLog
.filter(day => day.isExcluded && day.wouldHaveQualified)
.length;
// 3. Coin adjustments for this period
const adjustments = coinAdjustments
.filter(adj => adj.period === period && adj.student_id === student.id)
.reduce((sum, adj) => sum + adj.adjustment_amount, 0);
return baseCoins + exemptCredits + adjustments;
}
// Total across all periods
totalCoins = periods.reduce((sum, period) =>
sum + calculatePeriodCoins(student, period), 0
);// 1. Student submits request
POST /api/student/requests
β request_type: 'assignment_replacement' | 'quiz_replacement' | 'override_request'
β Insert into student_requests table
β Status: 'pending'
// 2. Admin views and processes request
GET /api/admin/requests
β Returns all requests sorted by section, name
β Filter by section and/or request type
// 3. Admin updates request
PUT /api/admin/requests
// For override requests (approved):
β If request_type === 'override_request' and status === 'approved':
β Insert into student_day_overrides table
β override_type = 'qualified'
β Student's day status and coins recalculated automatically
// For redemption requests (completed/approved):
β If coinDeduction > 0 and status is 'approved'/'completed':
β Insert into coin_adjustments (negative amount)
β reason = "Request fulfilled: {type}. {admin_notes}"
β Update request status
// 4. Student sees changes immediately
GET /api/student
β For overrides: Day status updated in dailyLog
β For redemptions: Coins deducted and shown in adjustments// Excel file structure expected
interface ExcelRow {
'Student Name': string
'Email': string
'Student ID': string
'h:mm_1', 'h:mm_2', ... // Time columns
'added to pie_1', 'added to pie_2', ... // Topic columns
}
// Processing algorithm
1. Parse Excel file using XLSX library
2. Extract student metadata (name, email, ID)
3. Process daily columns (minutes, topics)
4. Calculate qualification per day:
- Qualified if: minutes >= 31 AND topics >= 1
5. Apply period-specific excluded dates
6. Calculate exempt day credits
7. Store as JSONB in student_data tableStudentLookup Component
βββ State Management
β βββ studentInfo: StudentInfo | null
β βββ studentPeriods: PeriodInfo[]
β βββ coinAdjustments: CoinAdjustment[]
β βββ totalCoinsAcrossPeriods: number
βββ API Calls
β βββ POST /api/student (fetch student data)
β βββ GET /api/analytics (class stats)
βββ Child Components
βββ CalendarView (daily progress grid)
βββ RedemptionModal (coin redemption form)
βββ CompletionChart (analytics visualization)RedemptionModal Component
βββ Props
β βββ redemptionType: 'assignment' | 'quiz'
β βββ studentId, studentName, studentEmail
β βββ period, sectionNumber
βββ Form State
β βββ assignmentName: string
β βββ courseSection: string
β βββ additionalNotes: string
βββ Submission Flow
βββ Validate form data
βββ POST /api/student/requests
βββ request_type = 'assignment_replacement' | 'quiz_replacement'
βββ request_details = formatted string with all infoAdminRequestsPage Component
βββ State Management
β βββ requests: StudentRequest[]
β βββ selectedSection: string (filter)
β βββ selectedStatus: string (filter)
βββ Filtering Logic
β βββ Sort by: section_number ASC, student_name ASC
βββ Update Modal
β βββ Status dropdown
β βββ Admin notes textarea
β βββ Coin deduction input (appears for approved/completed)
βββ API Calls
βββ GET /api/admin/requests
βββ PUT /api/admin/requests// Server-side password validation
if (password !== process.env.ADMIN_PASSWORD) {
return NextResponse.json({ error: "Invalid password" }, { status: 401 });
}
// Client-side password persistence
localStorage.setItem('adminPassword', password);// Students can only access their own data
// No authentication - uses student ID as key
// Student ID normalized: studentId.toLowerCase().trim()// All endpoints validate required fields
if (!studentId || typeof studentId !== "string") {
return NextResponse.json({ error: "Invalid input" }, { status: 400 });
}
// Coin adjustment validation
if (typeof adjustmentAmount !== 'number' || isNaN(adjustmentAmount)) {
return NextResponse.json({ error: "Invalid amount" }, { status: 400 });
}// Students tracked across multiple exam periods
// Each period has its own:
// - Base coins from daily completions
// - Exempt day credits
// - Coin adjustments
// Total coins = sum of all period totals// Coin adjustments never hard-deleted
// Instead: is_active = false
// Allows audit trail and potential recovery
DELETE /api/admin/coin-adjustments
β UPDATE coin_adjustments SET is_active = false WHERE id = ?// When admin processes request with coin deduction:
// 1. Create coin_adjustments record with negative amount
// 2. Link via auto-generated reason
// 3. Student sees adjustment with clear explanation
reason = `Request fulfilled: ${requestType}. ${adminNotes}`// Day overrides applied per-student
// Two types:
// - 'qualified': Force day to qualified
// - 'disqualified': Force day to disqualified
// Stored in student_day_overrides table
// Applied during coin calculation// Aggregated from all student_data
interface Analytics {
period: string
sections: string[]
totalStudents: number
averageCompletion: number
dayStats: {
day: number
averageCompletion: number
qualifiedStudents: number
sectionBreakdown: SectionStats[]
}[]
}// Per student per period
percentComplete = (qualifiedDays / totalWorkingDays) * 100
extraCreditEligible = percentComplete >= 90
// Working days excludes:
// - Future days (day > totalDays)
// - Excluded dates (holidays, etc)# Vercel deployment
vercel env add ADMIN_PASSWORD
vercel env add POSTGRES_URL-- Run on first deployment
-- Tables created automatically via CREATE TABLE IF NOT EXISTS
-- See Database Schema section for full SQLThis project is private and proprietary. All rights reserved.
Built with β€οΈ for educational excellence