Skip to content

Latest commit

Β 

History

45 Commits

Folders and files

NameName
Last commit message
Last commit date
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

JMR Portal

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 πŸ‘ πŸ‘

image image image

Features

  • 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

Tech Stack

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
Email 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

File Structure

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

Database Schema

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

User Roles

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 (Pricing)

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

Local Development Setup

Prerequisites

  • Python 3.11+
  • Node.js 20+
  • PostgreSQL 15+

1. Database

# 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.sql

2. Backend

cd 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/docs

3. Frontend

cd frontend

npm install

cp .env.example .env

npm run dev
# App available at http://localhost:5173

4. First-time superadmin setup

With 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.


Environment Variables

Backend (backend/.env)

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-receipts

Frontend (frontend/.env)

VITE_API_URL=http://localhost:8000

API Overview

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.


Auth Flows

Login

  1. POST /auth/login β†’ returns access token (480 min) + refresh token (7 days)
  2. Frontend stores tokens; axios interceptor auto-refreshes on 401

Forgot Password

  1. POST /auth/forgot-password β€” generates a 15-minute token, sends reset link via email
  2. POST /auth/reset-password β€” validates token, sets new password, deletes token

Parent Onboarding

  1. Admin registers parent β†’ users record auto-created with password = null
  2. Superadmin calls POST /parents/{id}/send-onboarding β†’ 30-minute token sent via email
  3. Parent clicks link β†’ password setup form β†’ POST /auth/reset-password
  4. Parent can now log in and view their portal

Infrastructure

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

Production Deployment Checklist

  • Create jmr PostgreSQL user and jmrportal database with production password
  • Run db/init.sql to create schema
  • Configure backend .env with 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 /setup to create superadmin account
  • Add service types via Manage Services in the UI
  • End-to-end test before going live

License

MIT

About

Instead of manual updating and managing records that needed a bunch of book. I created this to help my parent to just update it from this web app

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages