Add documentation: user, admin, developer, and deployment guides

docs/user-guide.md    – sign-in, Members/Cashier/Bar tabs, receipts, FAQ
docs/admin-guide.md   – everything in user guide plus Admin tab, settings
                        reference, correcting transactions, DB backup/queries
docs/developer-guide.md – stack, project layout, DB schema, settings system,
                        auth system, full API reference, extension guide
docs/deployment.md    – internal network setup for Linux/Mac/Windows:
                        prerequisites, running the server, finding the IP,
                        connecting other devices, auto-start (systemd /
                        launchd / Task Scheduler), static IP, port change,
                        security notes

https://claude.ai/code/session_01JuRTR5Xjx8emQsyerBgGU7
This commit is contained in:
Claude 2026-05-30 12:10:49 +00:00
parent 45e10422c8
commit 034c882425
No known key found for this signature in database
4 changed files with 909 additions and 0 deletions

170
docs/admin-guide.md Normal file
View file

@ -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;
```

354
docs/deployment.md Normal file
View file

@ -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
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.clubledger</string>
<key>ProgramArguments</key>
<array>
<string>/Users/youruser/ClubLedger/.venv/bin/python</string>
<string>/Users/youruser/ClubLedger/main.py</string>
</array>
<key>WorkingDirectory</key>
<string>/Users/youruser/ClubLedger</string>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<true/>
<key>StandardOutPath</key>
<string>/Users/youruser/ClubLedger/clubledger.log</string>
<key>StandardErrorPath</key>
<string>/Users/youruser/ClubLedger/clubledger.log</string>
</dict>
</plist>
```
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://<server-ip>: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 |

270
docs/developer-guide.md Normal file
View file

@ -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 <repo>
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=<term>` 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.

115
docs/user-guide.md Normal file
View file

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