An admin management system for a small tuition and Quran-reading service. Tracks parents, their children, monthly fee payments, and overdue balances. Includes a parent-facing portal for viewing enrolment status and payment history.
Im planning to change framework to ASP.NET to learn more about ASP.NET π π
- Parent & child management β register parents with multiple phone numbers, add children with service type assignment
- Payment tracking β record payment sessions covering multiple children and months in one visit; track one-time registration fees separately
- Overdue monitoring β instant view of which children have unpaid months or outstanding registration fees
- Reports β filterable payment history by parent, child, month and year; export to PDF
- Role-based access β superadmin, admin, and parent (user) roles with separate dashboard views
- Parent onboarding β invite parents via email link; they set their own password on first login
- Receipt storage β upload PDF/image receipts directly to Cloudflare R2 via presigned URLs
- Prayer times widget β live Malaysian prayer times (waktusolat.app API) on the dashboard, auto-detects JAKIM zone from browser location with manual override
- Bilingual UI β English and Bahasa Melayu, switchable per user in profile settings
| Layer | Technology |
|---|---|
| Frontend | React 19 + TypeScript, Vite 8, Tailwind CSS v4 |
| Backend | FastAPI 0.138, Python, psycopg2 (raw SQL β no ORM) |
| Database | PostgreSQL |
| Auth | PyJWT β access token (480 min) + refresh token (7 days), bcrypt password hashing |
Resend β from jmr@dev-r.org |
|
| File Storage | Cloudflare R2 via presigned PUT/GET (boto3) |
| PDF Export | jsPDF + jspdf-autotable |
| i18n | i18next + react-i18next |
| Icons | Lucide React |
| Toasts | Sonner |
| Prayer Times | Waktu Solat API |
jmr-portal/
βββ backend/
β βββ app/
β β βββ main.py # FastAPI entry point, CORS, router registration
β β βββ config.py # All env vars loaded here β import from here only
β β βββ database.py # psycopg2 connection pool, get_db() dependency
β β βββ emails.py # Resend client + HTML email templates
β β βββ routers/
β β βββ auth.py # login, refresh, setup, forgot/reset-password
β β βββ parents.py # parent CRUD + send-onboarding
β β βββ children.py # children CRUD + toggle active/inactive
β β βββ payments.py # sessions, registration, overdue, receipt presigned URLs
β β βββ reports.py # payment-summary + my-payment-summary
β β βββ users.py # user management (admin/staff accounts)
β β βββ dependencies.py # get_current_user, require_admin, require_superadmin
β βββ requirements.txt
β βββ .env # never committed β see .env.example
β βββ .env.example
β
βββ frontend/
β βββ public/
β β βββ logo.png # JMR logo (used as favicon)
β βββ src/
β β βββ api/ # API client functions (one file per domain)
β β β βββ client.ts # axios instance + token refresh interceptor
β β β βββ auth.ts
β β β βββ parents.ts
β β β βββ children.ts
β β β βββ payments.ts
β β β βββ reports.ts
β β β βββ users.ts
β β βββ components/ # Shared UI components
β β β βββ Layout.tsx # App shell with sticky navbar
β β β βββ Navbar.tsx
β β β βββ Sidebar.tsx
β β β βββ PrayerTimes.tsx # Prayer times widget (waktusolat.app API)
β β β βββ Modal.tsx
β β β βββ Button.tsx
β β β βββ Input.tsx
β β β βββ FormField.tsx
β β β βββ Card.tsx
β β β βββ Badge.tsx
β β β βββ Table.tsx
β β β βββ PageHeader.tsx
β β βββ hooks/
β β β βββ useAuth.tsx # Auth context + token management
β β βββ i18n/
β β β βββ index.ts # i18next setup
β β β βββ locales/
β β β βββ en.json # English translations
β β β βββ ms.json # Bahasa Melayu translations
β β βββ pages/
β β β βββ Login.tsx
β β β βββ Setup.tsx # One-time superadmin account creation
β β β βββ ForgotPassword.tsx
β β β βββ ResetPassword.tsx # Shared by forgot-password + parent onboarding
β β β βββ Dashboard.tsx # Admin + parent dashboard views
β β β βββ Parents.tsx
β β β βββ ParentDetail.tsx # Full parent record: children, payment history, edit
β β β βββ Payments.tsx # Overdue monthly + unpaid registration fees
β β β βββ Reports.tsx # Admin payment reports + PDF export
β β β βββ Services.tsx # Service type management
β β β βββ Users.tsx # Admin/staff account management
β β β βββ Profile.tsx # Edit profile, change password, language preference
β β β βββ MyChildren.tsx # Parent view β their enrolled children
β β β βββ MyPayments.tsx # Parent view β outstanding fees
β β β βββ MyReports.tsx # Parent view β payment history
β β βββ types/
β β β βββ index.ts # Shared TypeScript types
β β βββ config.ts # Frontend env vars (API base URL)
β β βββ App.tsx # Route definitions
β β βββ main.tsx
β β βββ index.css # Tailwind + CSS custom properties (design tokens)
β βββ index.html
β βββ vite.config.ts
β βββ tsconfig.json
β βββ package.json
β
βββ db/
β βββ init.sql # Idempotent schema β safe to re-run, no data loss
β βββ migration/ # Sequential SQL files applied during development
β βββ 001_add_service_types.sql
β βββ 002_add_is_active_to_children.sql
β βββ 003_add_payment_method_and_registration.sql
β βββ 004_add_superadmin_role.sql
β βββ 005_add_user_id_to_parents.sql
β βββ 006_nullable_password_email_users.sql
β βββ 007_add_paid_at_to_registration_payments.sql
β βββ 008_add_is_active_to_service_types.sql
β βββ 009_child_services_junction.sql
β βββ 010_add_is_active_to_parents.sql
β βββ 011_add_password_reset_tokens.sql
β βββ 012_add_service_description.sql
β βββ 013_add_receipt_key.sql
β βββ 014_add_user_language.sql
β
βββ CLAUDE.md # AI coding assistant instructions
βββ LICENSE
βββ README.md
roles id, name ('superadmin' | 'admin' | 'user')
users id, name, email (unique), address, phone_num,
password (bcrypt, nullable = not yet activated),
role_id β roles, language ('en'|'ms'), created_at
parents id, user_id β users (nullable), parent_name,
email (unique, nullable), address, is_active, created_at
phone_numbers id, parent_id β parents, phone_num
-- at least 1 required per parent
children id, parent_id β parents, name, dob (nullable),
service_type_id β service_types,
is_active (default TRUE), created_at
-- never deleted; toggle is_active to deactivate
service_types id, name (unique), monthly_fee, registration_fee,
description (nullable), is_active, created_at
password_reset_tokens id, user_id β users, token, expires_at, created_at
-- shared by forgot-password (15 min) and onboarding (30 min)
-- deleted after use
payment_sessions id, parent_id β parents, total_amount,
payment_method ('cash'|'bank_transfer'|'online'),
notes, receipt_key (R2 object key, nullable),
paid_at, created_by β users, created_at
fee_payments id, session_id β payment_sessions, child_id β children,
month, year, amount, created_at
UNIQUE (child_id, month, year)
registration_payments id, child_id β children (UNIQUE), amount,
payment_method, paid_at, created_by β users, created_at| Role | Created by | Can do |
|---|---|---|
superadmin |
POST /auth/setup (one-time) |
Everything β including creating admin accounts and sending parent onboarding links |
admin |
superadmin | Create/manage parents, children, payments, staff accounts |
user |
Auto-created when a parent is registered | Parent portal β view their own children, payments, reports |
Services are managed via the Manage Services UI β no code changes needed to add new ones.
| Service | Monthly | Registration |
|---|---|---|
| Quran Reading only | RM 30 | RM 20 |
| Tuition + Quran Reading | RM 200 | RM 50 |
- Python 3.11+
- Node.js 20+
- PostgreSQL 15+
# Create database and user
psql -U postgres -c "CREATE USER jmr WITH PASSWORD 'yourpassword';"
psql -U postgres -c "CREATE DATABASE jmrportal OWNER jmr;"
# Run schema
psql -U jmr -d jmrportal -f db/init.sqlcd backend
# Create and activate virtual environment
python -m venv venv
venv\Scripts\activate # Windows
# source venv/bin/activate # macOS/Linux
pip install -r requirements.txt
# Copy and fill in environment variables
cp .env.example .env
# Start dev server
uvicorn app.main:app --reload
# API available at http://localhost:8000
# Swagger docs at http://localhost:8000/docscd frontend
npm install
cp .env.example .env
npm run dev
# App available at http://localhost:5173With both servers running, visit http://localhost:5173 and navigate to /setup to create the superadmin account. The endpoint is permanently disabled after the first use.
DATABASE_URL=postgresql://jmr:yourpassword@localhost:5432/jmrportal
SECRET_KEY=<64-char random hex>
ALGORITHM=HS256
ACCESS_TOKEN_EXPIRE_MINUTES=480
ALLOWED_ORIGINS=http://localhost:5173
RESEND_API_KEY=re_xxxxx
EMAIL_FROM=jmr@dev-r.org
FRONTEND_URL=http://localhost:5173
R2_ENDPOINT_URL=https://<account>.r2.cloudflarestorage.com
R2_ACCESS_KEY_ID=xxxxx
R2_SECRET_ACCESS_KEY=xxxxx
R2_BUCKET_NAME=jmr-receiptsVITE_API_URL=http://localhost:8000| Group | Base path | Description |
|---|---|---|
| Auth | /auth |
Login, token refresh, setup, forgot/reset password |
| Parents | /parents |
CRUD, send onboarding link |
| Children | /children |
CRUD, toggle active/inactive |
| Payments | /payments |
Sessions, registration fees, overdue, receipt presigned URLs |
| Reports | /reports |
Payment summary (admin + parent views) |
| Users | /users |
Admin/staff management, parent activation |
Full interactive docs available at /docs when the backend is running.
POST /auth/loginβ returns access token (480 min) + refresh token (7 days)- Frontend stores tokens; axios interceptor auto-refreshes on 401
POST /auth/forgot-passwordβ generates a 15-minute token, sends reset link via emailPOST /auth/reset-passwordβ validates token, sets new password, deletes token
- Admin registers parent β
usersrecord auto-created withpassword = null - Superadmin calls
POST /parents/{id}/send-onboardingβ 30-minute token sent via email - Parent clicks link β password setup form β
POST /auth/reset-password - Parent can now log in and view their portal
| Item | Details |
|---|---|
| VPS | Hetzner CX23 β 2 vCPU, 4 GB RAM, 40 GB SSD, Ubuntu 24.04 LTS |
| Database | PostgreSQL on the same VPS |
| Backend | FastAPI via uvicorn, managed by systemd |
| Frontend | Built with npm run build, served by Nginx |
| Reverse proxy | Nginx β routes /api β FastAPI, serves React SPA for all other paths |
| SSL | Let's Encrypt via Certbot |
| Receipt storage | Cloudflare R2 β direct client upload via presigned PUT URL (5 min expiry); retrieval via presigned GET URL (15 min) |
| DB access (prod) | pgAdmin on local machine β SSH tunnel β VPS PostgreSQL |
- Create
jmrPostgreSQL user andjmrportaldatabase with production password - Run
db/init.sqlto create schema - Configure backend
.envwith production credentials - Set up systemd service for uvicorn
- Build frontend:
npm run build - Configure Nginx to serve frontend and reverse-proxy API
- Obtain SSL certificate with Certbot
- Visit
/setupto create superadmin account - Add service types via Manage Services in the UI
- End-to-end test before going live
MIT