diff --git a/docs/admin-guide.md b/docs/admin-guide.md new file mode 100644 index 0000000..c59f711 --- /dev/null +++ b/docs/admin-guide.md @@ -0,0 +1,170 @@ +# ClubLedger – Administrator Guide + +Administrators have access to everything in the [Staff User Guide](user-guide.md) plus the **Admin** tab. + +--- + +## First Login + +On first startup the system creates a default admin account: + +| Username | Password | +|---|---| +| `admin` | `admin` | + +**Change this password immediately.** Go to **Admin → Staff Accounts**, find the admin row, click **Edit**, and set a strong password. + +--- + +## Admin Tab + +The Admin tab contains two sections: **App Settings** and **Staff Accounts**. + +--- + +## App Settings + +These settings control how ClubLedger looks and behaves. Changes take effect immediately without restarting the server. + +### Club Identity + +| Setting | Description | +|---|---| +| **Club Name** | Appears in the navigation bar, on receipts, and on statements | + +### Currency + +ClubLedger stores all monetary values as integers in a minor unit (e.g. pence) internally. The settings below control how amounts are displayed and entered. + +| Setting | Example | Description | +|---|---|---| +| **Currency Symbol** | `£`, `$`, `€`, `฿` | Prepended to every displayed amount | +| **Currency Name** | `pounds` | The major unit. Shown in amount field labels. Users enter amounts in this unit. | +| **Subunit Name** | `pence` | The minor unit stored in the database. Used in internal labels only. | +| **Subunits per unit** | `100` | How many minor units make one major unit. 100 for most currencies, 1 for currencies with no subunit. | + +> **Important:** If you change **Subunits per unit**, all existing balances will be re-displayed using the new divisor. The stored integers do not change. Only change this on a fresh installation. + +### Transaction Limits + +All limits are entered in the **major unit** (e.g. pounds). + +| Setting | Description | +|---|---| +| **Minimum top-up** | Cashier cannot top up less than this amount | +| **Maximum top-up** | Cashier cannot top up more than this in a single transaction | +| **Maximum single charge** | Bar cannot charge more than this in a single transaction | + +### Receipt Footer + +Optional text printed at the bottom of every receipt and statement. Useful for: +- A thank-you message +- A refund or returns policy +- Contact details + +Accepts plain text. Line breaks are preserved. + +### Allow Negative Balance (Overdraft) + +When ticked, the bar can charge a member even if their balance would go below zero. When unticked (the default), charges are blocked if the member has insufficient funds. + +--- + +## Staff Accounts + +### Creating an Account + +Fill in the **Add Account** form at the bottom of the Staff Accounts panel: + +| Field | Notes | +|---|---| +| Name | The person's real name — appears on receipts and transaction logs | +| Username | Used to sign in. Lowercase letters and numbers recommended. | +| Password | Minimum length enforced by the browser. Choose something strong. | +| Role | **Staff** or **Admin** — see below | + +### Roles + +| Role | Capabilities | +|---|---| +| **Staff** | Members, Cashier, Bar tabs | +| **Admin** | Everything above, plus the Admin tab (settings and account management) | + +### Editing an Account + +Click **Edit** on any row. You can change: +- Name +- Username +- Password (leave blank to keep the current one) +- Role +- **Active** checkbox — untick to disable login without deleting the account + +### Deleting an Account + +Click **Delete**. You cannot delete your own account, and you cannot delete the last remaining admin account. + +### Resetting a Staff Member's Password + +Open the edit modal for their account, enter a new password, and save. The next time they log in they use the new password. There is no email reset — passwords are changed directly by an admin. + +--- + +## Managing Members + +### Correcting a Transaction + +There is no transaction editing or deletion by design (audit trail). To correct a mistake: + +- **Overcharged:** Apply a top-up for the difference, with a note explaining the correction. +- **Under-charged:** Apply a charge for the difference, with a note. +- **Wrong member charged:** Top up the affected member and charge the correct one, with matching notes on both. + +### Resetting a Member's PIN + +Members tab → click **Edit** on the row → enter a new PIN → Save. + +### Deleting a Member + +The **Delete** button only appears when a member's balance is exactly zero. Deletion is permanent and removes all transaction history for that member. If a member has a non-zero balance, bring it to zero first with a correcting transaction before deleting. + +### Printing Statements + +Members tab → click **Statement** on any row. Statements open in a new browser tab. Use the A4/A5 toggle before printing. + +--- + +## Backing Up Data + +All data is stored in a single SQLite file: `clubledger.db` in the application folder. To back up, simply copy this file to another location. + +``` +# Linux / Mac – copy to home directory +cp /path/to/ClubLedger/clubledger.db ~/clubledger-backup-$(date +%Y%m%d).db +``` + +The `staff.json` file stores the legacy staff name list (used only by the standalone `/cashier` and `/bar` pages, not the main app). Back this up too if you use those pages. + +To restore, stop the server, replace `clubledger.db` with the backup copy, and restart. + +--- + +## Checking Transaction Logs + +The database can be queried directly with any SQLite tool (e.g. [DB Browser for SQLite](https://sqlitebrowser.org/), which is free and cross-platform): + +```sql +-- All transactions in the last 7 days +SELECT m.name, m.member_number, l.type, l.amount, l.staff_name, l.created_at +FROM ledger_entries l +JOIN members m ON m.id = l.member_id +WHERE l.created_at >= datetime('now', '-7 days') +ORDER BY l.created_at DESC; + +-- Current balance for every member +SELECT m.name, m.member_number, + SUM(CASE WHEN l.type='topup' THEN l.amount ELSE -l.amount END) AS balance +FROM members m +LEFT JOIN ledger_entries l ON l.member_id = m.id +GROUP BY m.id +ORDER BY m.name; +``` diff --git a/docs/deployment.md b/docs/deployment.md new file mode 100644 index 0000000..e96da94 --- /dev/null +++ b/docs/deployment.md @@ -0,0 +1,354 @@ +# ClubLedger – Deployment Guide + +## Overview + +ClubLedger runs as a small web server on **one computer** (the server). Every other device on the same Wi-Fi network — tablets at the bar, a laptop at the cashier desk, a phone — opens the app in a normal web browser. No software is installed on the client devices. + +``` + Wi-Fi Router + │ + ┌────────────────┼────────────────┐ + │ │ │ + Server PC Cashier Tablet Bar Tablet + runs ClubLedger opens browser opens browser + :8000 http://192.168.1.x:8000 +``` + +--- + +## Part 1 – Initial Setup + +### Prerequisites by Operating System + +#### Linux (Debian / Ubuntu) + +```bash +sudo apt update +sudo apt install git python3 python3-venv python3-full +``` + +#### macOS + +Install [Homebrew](https://brew.sh/) if you haven't already, then: + +```bash +brew install git python3 +``` + +Alternatively, download Python from [python.org](https://www.python.org/downloads/) and install Git from [git-scm.com](https://git-scm.com/). + +#### Windows + +1. Download and install **Python 3.11+** from [python.org](https://www.python.org/downloads/). + - On the first installer screen, tick **"Add Python to PATH"** before clicking Install. +2. Download and install **Git** from [git-scm.com](https://git-scm.com/download/win). Accept all defaults. + +--- + +### Get the Code + +Open a terminal (Linux/Mac) or **Git Bash** / **Command Prompt** (Windows): + +```bash +# Clone the repository +git clone https://github.com/kbenestad/clubledger.git +cd ClubLedger + +# Or, if you downloaded a ZIP instead: +# Unzip it, then open a terminal in that folder +``` + +--- + +### Start the Server + +**Linux / macOS:** + +```bash +chmod +x run.sh +./run.sh +``` + +**Windows** — open **Command Prompt** or **PowerShell** in the project folder: + +```cmd +python -m venv .venv +.venv\Scripts\pip install -r requirements.txt +.venv\Scripts\python main.py +``` + +> On Windows you can also create a `run.bat` file with those three lines so double-clicking it starts the server in future. + +The first time it runs, the server prints the default admin credentials to the terminal: + +``` +============================================================ + Default admin created → username: admin password: admin + Change this immediately in the Admin → Staff Accounts area. +============================================================ +``` + +It then starts listening: + +``` +INFO: Uvicorn running on http://0.0.0.0:8000 +``` + +Open `http://localhost:8000` in a browser on the same machine to confirm it works. + +--- + +## Part 2 – Finding the Server's IP Address + +For other devices to connect, you need the **local IP address** of the server machine — the address your router has assigned to it on the Wi-Fi network. This is usually something like `192.168.1.42` or `10.0.0.15`. + +### Linux + +```bash +hostname -I +``` + +The first address listed is normally the right one. Example output: `192.168.1.42` + +Or with more detail: + +```bash +ip addr show | grep "inet " | grep -v "127.0.0.1" +``` + +### macOS + +Open **System Settings → Network → Wi-Fi → Details**. The IP address is listed there. + +Or in a terminal: + +```bash +ipconfig getifaddr en0 # Wi-Fi +ipconfig getifaddr en1 # Try this if the above gives nothing +``` + +### Windows + +Open **Command Prompt** and run: + +```cmd +ipconfig +``` + +Look for the section labelled **Wi-Fi** (or **Wireless LAN adapter Wi-Fi**). The value next to **IPv4 Address** is the server's address — for example `192.168.1.42`. + +--- + +## Part 3 – Connecting Other Devices + +Once you have the server's IP address, any device on the same Wi-Fi network can open ClubLedger by entering this address in any browser: + +``` +http://192.168.1.42:8000 +``` + +Replace `192.168.1.42` with your actual IP address. + +This works on: +- Other laptops and desktops (any browser) +- iPads and Android tablets +- Smartphones + +> **No app installation needed.** The browser is the client. + +--- + +## Part 4 – Keeping It Running + +By default, ClubLedger stops when you close the terminal. To keep it running continuously, set it up as a background service. + +### Linux – systemd Service + +Create a service file. Replace `/home/youruser/ClubLedger` with the actual path. + +```bash +sudo nano /etc/systemd/system/clubledger.service +``` + +Paste the following (adjust `User`, `WorkingDirectory`, and the path to Python): + +```ini +[Unit] +Description=ClubLedger Store Credit App +After=network.target + +[Service] +Type=simple +User=youruser +WorkingDirectory=/home/youruser/ClubLedger +ExecStart=/home/youruser/ClubLedger/.venv/bin/python main.py +Restart=on-failure +RestartSec=5 + +[Install] +WantedBy=multi-user.target +``` + +Enable and start: + +```bash +sudo systemctl daemon-reload +sudo systemctl enable clubledger +sudo systemctl start clubledger +``` + +Check it is running: + +```bash +sudo systemctl status clubledger +``` + +View logs: + +```bash +journalctl -u clubledger -f +``` + +ClubLedger now starts automatically when the computer boots. + +--- + +### macOS – launchd + +Create a file at `~/Library/LaunchAgents/com.clubledger.plist`. + +Adjust the paths below to match your actual username and ClubLedger folder: + +```xml + + + + + Label + com.clubledger + ProgramArguments + + /Users/youruser/ClubLedger/.venv/bin/python + /Users/youruser/ClubLedger/main.py + + WorkingDirectory + /Users/youruser/ClubLedger + RunAtLoad + + KeepAlive + + StandardOutPath + /Users/youruser/ClubLedger/clubledger.log + StandardErrorPath + /Users/youruser/ClubLedger/clubledger.log + + +``` + +Load it: + +```bash +launchctl load ~/Library/LaunchAgents/com.clubledger.plist +``` + +To stop it: + +```bash +launchctl unload ~/Library/LaunchAgents/com.clubledger.plist +``` + +Logs are written to `clubledger.log` in the project folder. + +--- + +### Windows – Task Scheduler + +1. Create a file called `start_clubledger.bat` in the ClubLedger folder: + +```bat +@echo off +cd /d "C:\Users\YourName\ClubLedger" +.venv\Scripts\python main.py +``` + +2. Open **Task Scheduler** (search for it in the Start menu). +3. Click **Create Basic Task…** +4. Name it `ClubLedger`. Click Next. +5. Trigger: **When the computer starts**. Click Next. +6. Action: **Start a program**. Click Next. +7. Browse to `start_clubledger.bat`. Click Next, then Finish. +8. Right-click the new task → **Properties** → **General** tab → tick **Run whether user is logged on or not**. + +ClubLedger will now start automatically at boot. + +To check it manually, right-click the task and choose **Run**. + +--- + +## Part 5 – Using a Fixed IP Address + +Router DHCP can change the server's IP over time, which would break the URL bookmarked on client devices. Prevent this by assigning a **static IP** to the server machine. + +### Option A – Reserve the IP in Your Router (Recommended) + +1. Log in to your router admin page (usually `http://192.168.1.1` or `http://192.168.0.1`). +2. Find **DHCP Reservations** or **Static DHCP** (varies by router brand). +3. Find the server's MAC address and assign it a fixed IP (e.g. `192.168.1.10`). + +The router always gives the same IP to that machine. No changes needed on the machine itself. + +### Option B – Static IP on the Machine + +See your operating system's network settings to assign a static IP manually. Use an address outside the router's DHCP range to avoid conflicts. Set the gateway to your router's IP and DNS to `8.8.8.8` or your router's IP. + +--- + +## Part 6 – Changing the Port + +If port 8000 is in use by something else, edit the last line of `main.py`: + +```python +uvicorn.run("main:app", host="0.0.0.0", port=8080, reload=True) +``` + +Or pass it as an argument: + +```bash +./run.sh --port 8080 # Linux / macOS +.venv\Scripts\python main.py --port 8080 # Windows +``` + +Devices would then connect to `http://192.168.1.42:8080`. + +--- + +## Part 7 – Security Notes + +ClubLedger is designed for a **trusted internal network** — a private club or venue Wi-Fi. It is not hardened for exposure to the public internet. + +| Risk | Mitigation | +|---|---| +| Staff accessing admin settings | Use separate staff and admin accounts; do not share admin credentials | +| Weak PINs | Encourage members to set at least 6-digit PINs | +| Data loss | Back up `clubledger.db` regularly (copy the file) | +| Unauthorised network access | Use WPA2/WPA3 Wi-Fi with a strong password; consider a separate staff VLAN if your router supports it | +| Accidental deletion | The delete button only appears on zero-balance accounts; there is no bulk delete | + +**Do not expose ClubLedger directly to the internet** (e.g. by port-forwarding port 8000 on your router) without first adding HTTPS and reviewing security. For internal-only use it is fine as-is. + +--- + +## Quick Reference + +| Task | Command | +|---|---| +| Start the server (Linux/Mac) | `./run.sh` | +| Start the server (Windows) | `.venv\Scripts\python main.py` | +| Find server IP (Linux) | `hostname -I` | +| Find server IP (Mac) | `ipconfig getifaddr en0` | +| Find server IP (Windows) | `ipconfig` → IPv4 Address under Wi-Fi | +| Access from another device | `http://:8000` | +| Stop the server | Press `Ctrl+C` in the terminal | +| View systemd logs (Linux) | `journalctl -u clubledger -f` | +| Backup data | Copy `clubledger.db` to a safe location | diff --git a/docs/developer-guide.md b/docs/developer-guide.md new file mode 100644 index 0000000..57d7517 --- /dev/null +++ b/docs/developer-guide.md @@ -0,0 +1,270 @@ +# ClubLedger – Developer Guide + +## Technology Stack + +| Layer | Technology | +|---|---| +| Backend | Python 3.11+ · FastAPI · SQLite (via stdlib `sqlite3`) | +| Auth | bcrypt password hashing · in-memory session tokens · httpOnly cookies | +| Frontend | Vanilla HTML/CSS/JS — no build step, no framework | +| Dependencies | `fastapi`, `uvicorn[standard]`, `bcrypt` | + +--- + +## Project Structure + +``` +ClubLedger/ +├── main.py # Entire backend — one file +├── requirements.txt # pip dependencies +├── run.sh # Start script (creates venv, installs deps, runs server) +├── clubledger.db # SQLite database (created on first run, git-ignored) +├── staff.json # Legacy staff name list (created on first use) +├── docs/ +│ ├── user-guide.md +│ ├── admin-guide.md +│ ├── developer-guide.md +│ └── deployment.md +└── static/ + ├── index.html # Main SPA (Members / Cashier / Bar / Admin tabs) + ├── style.css # All styles + ├── app.js # Main SPA logic + ├── common.js # Shared helpers (also used by standalone pages) + ├── cashier.html # Standalone cashier page (/cashier) + ├── cashier.js + ├── bar.html # Standalone bar page (/bar) + └── bar.js +``` + +`main.py` is intentionally a single file. It stays under ~450 lines because the domain is simple. Split it only if it grows substantially. + +--- + +## Running Locally + +```bash +git clone +cd ClubLedger +./run.sh # creates .venv, installs deps, starts server on :8000 +``` + +With auto-reload during development: + +```bash +source .venv/bin/activate +uvicorn main:app --reload --port 8000 +``` + +The default admin account (`admin` / `admin`) is printed to the console on first run. + +--- + +## Database Schema + +### `members` +| Column | Type | Notes | +|---|---|---| +| id | INTEGER PK | | +| member_number | TEXT UNIQUE | Human-readable ID | +| name | TEXT | | +| pin_hash | TEXT | bcrypt hash | +| created_at | TEXT | `datetime('now')` UTC | + +### `ledger_entries` +| Column | Type | Notes | +|---|---|---| +| id | INTEGER PK | | +| member_id | INTEGER FK | → members.id | +| amount | INTEGER | Minor currency units (e.g. pence) — always positive | +| type | TEXT | `topup` or `charge` | +| venue | TEXT | `cashier` or `bar` | +| note | TEXT | Optional free text | +| staff_name | TEXT | Name of logged-in staff at time of transaction | +| created_at | TEXT | UTC datetime | + +Balance is computed on-the-fly: `SUM(topups) - SUM(charges)`. There is no stored balance column — this avoids drift and makes the audit trail self-consistent. + +### `staff_accounts` +| Column | Type | Notes | +|---|---|---| +| id | INTEGER PK | | +| name | TEXT | Display name, used as `staff_name` on transactions | +| username | TEXT UNIQUE | Login credential | +| password_hash | TEXT | bcrypt hash | +| role | TEXT | `staff` or `admin` | +| active | INTEGER | 0 or 1 | +| created_at | TEXT | | + +### `products` +| Column | Type | Notes | +|---|---|---| +| id | INTEGER PK | | +| name | TEXT | | +| brand | TEXT | Optional | +| price | INTEGER | Minor units | +| member_price | INTEGER | Optional discounted price | +| search_tags | TEXT | Space/comma separated search terms | +| active | INTEGER | 0 or 1 | + +Products are searchable via `GET /products?q=` and managed via `POST /products`. No UI exists yet — use the API directly or SQLite directly to seed products. + +### `app_settings` +| Column | Type | +|---|---| +| key | TEXT PK | +| value | TEXT (JSON) | + +Settings are loaded at startup into the module-level `_settings` dict and re-read after every admin save. The `CONFIG` dict in `main.py` provides fallback defaults. + +--- + +## Settings System + +``` +CONFIG dict ← hard defaults in main.py + + +app_settings table ← admin overrides stored as JSON strings + ↓ +_settings dict ← merged at startup and after each /admin/settings POST + ↓ +format_amount() ← reads _settings at call time +/config endpoint ← returns _settings to the frontend on every page load +``` + +To add a new configurable value: +1. Add a default to `CONFIG` +2. Add the field to the `AppSettingsUpdate` Pydantic model +3. Add the input to the Admin settings form in `index.html` +4. Load and save it in `loadAdminSettings()` / `saveSettings()` in `app.js` + +--- + +## Auth System + +- `POST /auth/login` validates credentials against `staff_accounts`, creates a `secrets.token_hex(32)` token, stores it in the module-level `_sessions` dict, and sets it as an `httpOnly` cookie. +- All protected endpoints use `Depends(current_user)` which reads the cookie and looks up the session. +- Admin-only endpoints use `Depends(admin_user)` which calls `current_user` then checks `role == "admin"`. +- Sessions expire after 8 hours (configurable via `SESSION_TTL` in `main.py`). +- Sessions are lost on server restart (in-memory). This is intentional for simplicity; upgrade to a DB-backed session store if persistence is needed. +- Print views (`/receipt/`, `/members/*/statement`) deliberately have **no auth** — they are opened as pop-up tabs from an authenticated page. + +--- + +## API Reference + +All endpoints except `/config`, `/auth/login`, and the print views require a valid session cookie. + +### Auth + +| Method | Path | Body | Returns | +|---|---|---|---| +| POST | `/auth/login` | `{username, password}` | `{name, role}` + sets cookie | +| POST | `/auth/logout` | — | `{ok}` + clears cookie | +| GET | `/auth/me` | — | `{name, role}` | + +### Members + +| Method | Path | Notes | +|---|---|---| +| GET | `/members?q=` | List/search. Returns balance per member. | +| POST | `/members` | `{member_number, name, pin}` | +| PUT | `/members/{id}` | `{member_number?, name?, pin?}` — all optional | +| DELETE | `/members/{id}` | Blocked if balance ≠ 0 | +| GET | `/members/{id}/transactions` | `?limit=50&offset=0` | +| GET | `/members/{id}/statement` | Returns printable HTML | + +### Transactions + +| Method | Path | Body | +|---|---|---| +| POST | `/topup` | `{member_id, amount, note?}` — amount in minor units | +| POST | `/charge` | `{member_id, amount, pin, note?}` | + +Both return `{ok, entry_id, new_balance, new_balance_display}`. + +### Receipts + +| Method | Path | Notes | +|---|---|---| +| GET | `/receipt/{entry_id}` | Returns printable HTML. No auth. | + +### Products + +| Method | Path | Notes | +|---|---|---| +| GET | `/products?q=` | Search by name/brand/tags | +| POST | `/products` | `{name, brand?, price, member_price?, search_tags?}` | + +### Admin + +| Method | Path | Notes | +|---|---|---| +| GET | `/admin/settings` | Admin only | +| POST | `/admin/settings` | Admin only. Partial update — only sent fields are changed. | +| GET | `/admin/staff-accounts` | Admin only | +| POST | `/admin/staff-accounts` | `{name, username, password, role}` | +| PUT | `/admin/staff-accounts/{id}` | All fields optional | +| DELETE | `/admin/staff-accounts/{id}` | Cannot delete self or last admin | + +### Config (public) + +| Method | Path | Notes | +|---|---|---| +| GET | `/config` | Returns live `_settings`. Called by the frontend on every page load. | + +--- + +## Frontend Architecture + +`index.html` loads `common.js` then `app.js`. + +- **`common.js`** — shared utilities: `loadConfig()`, `apiFetch()`, `esc()`, `fmtAmount()`, `balanceClass()`, `setMsg()`, and the staff name dropdown helpers (used only by the standalone cashier/bar pages). +- **`app.js`** — all SPA logic: auth flow, tab switching, Members/Cashier/Bar/Admin views, modal management. + +The boot sequence in `app.js`: +1. `loadConfig()` — fetches `/config` so the login screen shows the club name. +2. `GET /auth/me` — if 401, show login overlay and stop. +3. On successful login or existing session: `startApp()` which wires up all event listeners and loads the members list. + +Amount conversion: `toMinor(inputId)` reads a decimal input and multiplies by `cfg.currency_divisor` before posting. All amounts sent to the API are in minor units. + +--- + +## Extending the App + +### Adding a new setting + +See the Settings System section above. + +### Adding a new transaction venue + +1. Add the new venue value to the `CHECK` constraint in the `ledger_entries` schema — requires a migration or database recreation. +2. Add a new endpoint (or extend `/topup`/`/charge` with a `venue` parameter). +3. Add a new tab or form in `index.html` / `app.js`. + +### Adding product management UI + +The `/products` API exists but only the GET endpoint is exposed in the main UI (products were removed from the bar tab). A products management panel in the Admin tab would follow the same pattern as Staff Accounts. + +### Persistent sessions + +Replace the `_sessions` dict with a `sessions` table in SQLite: + +```sql +CREATE TABLE sessions ( + token TEXT PRIMARY KEY, + user_id INTEGER REFERENCES staff_accounts(id), + expires TEXT +); +``` + +Adjust `current_user()` to query the table instead of the dict. + +--- + +## Environment Notes + +- The database file `clubledger.db` is created automatically in the working directory on first run. Add it to `.gitignore`. +- `staff.json` is also created in the working directory. Add to `.gitignore`. +- No environment variables are required. All configuration is in `CONFIG` (code) or `app_settings` (database). +- The app binds to `0.0.0.0:8000` by default — accessible from any device on the network. Pass `--host 127.0.0.1` to restrict to localhost only. diff --git a/docs/user-guide.md b/docs/user-guide.md new file mode 100644 index 0000000..3abfd3a --- /dev/null +++ b/docs/user-guide.md @@ -0,0 +1,115 @@ +# ClubLedger – Staff User Guide + +## What is ClubLedger? + +ClubLedger is a store-credit system for clubs and venues. Members load credit onto their account at the cashier desk, then spend it at the bar or other service points. All transactions are tracked and receipts are printed automatically. + +--- + +## Signing In + +Open the ClubLedger address in any web browser. You will see a sign-in screen. Enter the username and password given to you by your administrator, then click **Sign In**. + +Your name appears in the top-right corner of every screen while you are signed in. Click **Sign out** when you are done. + +> Sessions expire after 8 hours. The sign-in screen will reappear automatically when your session ends. + +--- + +## The Three Tabs + +The navigation bar at the top has three tabs: **Members**, **Cashier**, and **Bar**. Click a tab to switch between them. Administrators also see an **Admin** tab. + +--- + +## Members Tab + +Use this tab to register new members, look up existing members, and print account statements. + +### Registering a New Member + +Fill in the **Register New Member** form: + +| Field | Notes | +|---|---| +| Member Number | A unique ID for the member — a number, code, or anything you choose | +| Full Name | The member's name as it should appear on receipts | +| PIN | A secret 4-digit (or longer) code the member uses at the bar. Tell the member their PIN privately. | + +Click **Register**. The member appears in the table below. + +### Searching for a Member + +Type part of a name or member number into the search box and click **Search** (or press Enter). Leave the box empty and search to list everyone. + +### The Member Table + +Each row shows the member's number, name, current balance, and join date. + +| Button | What it does | +|---|---| +| **Statement** | Opens a printable full transaction history in a new tab | +| **Edit** | Change the member's name, number, or PIN | +| **Delete** | Only appears when balance is exactly zero. Permanently removes the member. | + +### Editing a Member + +Click **Edit** on any row. A panel appears with the current name and member number pre-filled. Change what you need. Leave the **New PIN** field blank to keep their current PIN. Click **Save**. + +### Printing a Statement + +Click **Statement** to open the statement in a new tab. Use the **A4 / A5** toggle to choose the paper size, then click **Print Statement**. + +--- + +## Cashier Tab + +Use this tab to add credit to a member's account (top-up). + +### How to Top Up + +1. Search for the member by name or number and click their row. +2. The selected member's name and current balance appear at the top of the form. +3. Enter the **Amount** — type it in the major currency unit (e.g. `10.00` for ten pounds). +4. Add an optional **Note** (e.g. "cash payment", "card payment"). +5. Click **Top Up**. + +A receipt opens automatically in a new tab. Print it or close it. + +If you need to start over, click **Cancel** to deselect the member. + +### Receipts + +Receipts show: member name and number, transaction type, amount, balance after, staff name, timestamp, and any footer text set by the administrator. + +Use the **A4 / A5** toggle at the top of the receipt page before printing. + +--- + +## Bar Tab + +Use this tab to charge a member's account (debit). The member must enter their PIN. + +### How to Charge + +1. Search for the member and click their row. +2. Enter the **Amount** to charge (e.g. `3.50`). +3. The member enters their **PIN** into the field. +4. Add an optional **Note** (e.g. the item name). +5. Click **Charge**. + +If the PIN is wrong, an error appears and nothing is charged. If the balance is insufficient, the charge is also blocked (unless the administrator has enabled overdraft). + +A receipt opens automatically in a new tab on a successful charge. + +--- + +## Common Questions + +**The member forgot their PIN.** An administrator can reset it: Members tab → Edit → enter a new PIN. + +**I topped up the wrong amount.** Contact an administrator. There is no undo button — a correcting charge or top-up must be applied manually and noted. + +**The receipt tab didn't open.** Your browser may be blocking pop-ups. Allow pop-ups for this site in your browser settings, or navigate directly to the statement page via Members → Statement. + +**The balance shows in the wrong currency.** Contact your administrator to update the currency settings in the Admin area.