# Persona Website — Personal Payroll System

Personal website starting with payroll management. Will expand to portfolio and other personal needs.

## Tech Stack
- Backend: Python 3.12 (local dev) · FastAPI · SQLAlchemy 2.0 · Alembic · PostgreSQL
- Frontend: React 18 · TypeScript · Vite · PrimeReact (Sakai template)
- Mobile: Flutter (planned — `mobile/` folder, not yet created)
- Backend port: **8002**
- Frontend port: **8003**
- Language: All code, comments, and UI labels in **English**

> ⚠️ **Server Python compatibility**: Production server (Ubuntu 20.04) runs Python 3.8.
> Code must stay Python 3.8-compatible: use `Optional[X]` not `X | None`, `List[X]` not `list[X]`.
> Python 3.12 is not available on Ubuntu 20.04 (focal) via deadsnakes PPA.

## Development Commands

### Backend (from backend/)
```bash
source venv/bin/activate
uvicorn app.main:app --reload --port 8002
```

### Frontend (from frontend/)
```bash
npm run dev   # starts on port 8003
```

### Database Migrations (from backend/)
```bash
alembic revision --autogenerate -m "description"
alembic upgrade head
alembic downgrade -1
```

### Create Admin User (one-time, from backend/)
```bash
source venv/bin/activate
python -c "
from app.core.database import SessionLocal
from app.models.user import User
from app.core.security import hash_password
import os
db = SessionLocal()
u = User(username=os.getenv('ADMIN_USERNAME','admin'), hashed_password=hash_password(os.getenv('ADMIN_PASSWORD','changeme')))
db.add(u); db.commit(); db.close(); print('Admin created')
"
```

## Architecture

### Project structure
```
personal.com/
├── CLAUDE.md
├── backend/          # FastAPI — shared by web and mobile
├── frontend/         # React/Vite — web app
├── mobile/           # Flutter — mobile app (planned, not yet created)
└── deploy/           # Nginx config, deployment scripts
```

### Backend structure
```
backend/app/
├── main.py          # FastAPI app, CORS, router registration
├── core/
│   ├── database.py  # SQLAlchemy engine + get_db()
│   └── security.py  # JWT + bcrypt + HTTPBearer
├── models/          # SQLAlchemy ORM models
├── schemas/         # Pydantic request/response schemas
├── routes/          # FastAPI routers (one per domain)
└── helpers/
    ├── crud.py      # get_or_404 helper
    └── csv_export.py # Kopramandiri CSV builder
```

### Frontend structure
```
frontend/src/
├── api/client.ts    # Axios + Bearer interceptor + 401 auto-logout
├── types/           # TypeScript interfaces per domain
├── services/        # Per-domain API call functions
├── components/
│   ├── layout/      # Sakai AppLayout, AppMenu, ProtectedRoute
│   └── shared/      # CurrencyDisplay, StatusBadge
├── utils/
│   └── date.ts      # toLocalDateString() helper
└── pages/           # One folder per domain
    ├── auth/
    ├── dashboard/
    ├── companies/
    ├── employees/
    ├── salary/
    └── payroll/
```

## Conventions
- All PKs: String (UUID4)
- Monetary amounts: Integer (whole IDR)
- JWT token in localStorage as `payroll_token`
- API prefix: `/api/*`
- CORS: allows `http://localhost:8003` (dev only — production uses same-origin via Nginx proxy)
- Loan.remaining_balance: denormalized, updated on each transaction
- Combined loan ledger: GET /api/employees/{id}/loan-ledger

## API Endpoints

### Auth
```
POST /api/auth/login             → { access_token, token_type, username }
GET  /api/auth/me                → current user info
POST /api/auth/change-password   → { current_password, new_password }
```

### Companies (protected)
```
GET/POST         /api/companies
GET/PUT/DELETE   /api/companies/{id}
```

### Employees (protected)
```
GET/POST         /api/employees
GET/PUT/DELETE   /api/employees/{id}
GET              /api/employees/table                  # list with benefit, loan_monthly, current_salary
GET/POST         /api/employees/{id}/salary-history
GET              /api/employees/{id}/loans
GET              /api/employees/{id}/loan-ledger       # combined running balance across ALL loans
GET              /api/employees/{id}/payment-history   # payroll export history (from payroll_runs)
```

### Salary (protected)
```
GET  /api/salary/recommendations   # AI pre-fill (weighted avg of past increases)
POST /api/salary/bulk-increase     # body: { items: [{ employee_id, new_base_salary, effective_date, notes }] }
```

### Loans (protected)
```
POST             /api/employees/{id}/loans
GET/PUT/DELETE   /api/loans/{id}
GET/POST         /api/loans/{id}/transactions
```

### Payroll (protected)
```
GET  /api/payroll/preview     # ?month&year&company_id&employee_ids
POST /api/payroll/export      # unified: { export_type, month, year, company_id, employee_ids, overrides?, record_transactions? }
POST /api/payroll/export-csv  # legacy: Kopramandiri CSV (regular payroll)
POST /api/payroll/export-thr  # legacy: Kopramandiri CSV (THR, base salary only)
GET  /api/payroll/runs        # list past payroll runs
GET  /api/payroll/runs/{id}   # detail of a payroll run
```

## Key Packages

### Backend `requirements.txt`
```
fastapi==0.115.0
uvicorn[standard]==0.30.6
sqlalchemy==2.0.35
alembic==1.13.2
psycopg2-binary==2.9.9
python-jose[cryptography]==3.3.0
passlib[bcrypt]==1.7.4
bcrypt==4.0.1          # PIN THIS — bcrypt 5.x is incompatible with passlib 1.7.4
python-multipart==0.0.12
python-dotenv==1.0.1
pydantic==2.9.2
pydantic-settings==2.5.2
```

### Frontend key packages
```
primereact · primeicons · primeflex
react-router-dom v6 · @tanstack/react-query v5 · axios · date-fns
```

## Deployment

### Live URL
`https://me.goteku.com` — protected by Nginx basic auth (username: `me`)

### Server
- OS: Ubuntu 20.04 (focal)
- Python: 3.8.x (3.12 not available on focal via deadsnakes PPA)
- PostgreSQL: 12
- Other services: Odoo 16 on port 8069 (separate venv, separate Nginx server block)

### Server paths
```
/var/www/html/me.goteku.com/
├── backend/          # FastAPI app + venv
│   ├── .env          # DATABASE_URL, SECRET_KEY, ADMIN_USERNAME, ADMIN_PASSWORD
│   └── venv/         # isolated venv (NOT shared with Odoo)
└── frontend/
    └── dist/         # built React app (served by Nginx as static files)
```

### Backend .env (server)
```
DATABASE_URL=postgresql://payroll_user:y1%26clZaz@localhost/personal_payroll_db
SECRET_KEY=<32-byte hex>
ADMIN_USERNAME=admin
ADMIN_PASSWORD=changeme
```
> ⚠️ `&` in DB password must be URL-encoded as `%26` in DATABASE_URL

### Nginx config
`/etc/nginx/sites-available/me.goteku.com` (two-block config)
- HTTP (port 80) → HTTPS redirect
- HTTPS (port 443): serves `frontend/dist` as static SPA, proxies `/api/` to `127.0.0.1:8002`
- Basic auth on entire site; `auth_basic off` inside `location /api/` (prevents JWT 401 → re-auth loop)
- Basic auth file: `/etc/nginx/.htpasswd`

### Deploy commands (run on server)
```bash
# Deploy frontend
cd /var/www/html/me.goteku.com/frontend
npm run build

# Run migrations after backend changes
cd /var/www/html/me.goteku.com/backend
source venv/bin/activate
alembic upgrade head

# Restart backend
pkill -f "uvicorn app.main"
cd /var/www/html/me.goteku.com/backend
source venv/bin/activate
nohup uvicorn app.main:app --host 127.0.0.1 --port 8002 > /tmp/uvicorn.log 2>&1 &
```

### CORS for mobile (Flutter)
When the Flutter mobile app is built, add the app's origin (or use `*` for native mobile) to `allow_origins` in `backend/app/main.py`:
```python
allow_origins=["http://localhost:8003", "*"]  # or specific origin for Flutter web
```
Native Flutter apps (Android/iOS) don't use browser CORS — no backend change needed for native mobile.
