invoice/CLAUDE.md
Claude d0c5249bcb
Add user, admin, dev documentation and CLAUDE.md
docs/user-guide.md  — end-user guide covering the full invoice workflow
docs/admin-guide.md — config.yml reference for setting up the app
docs/dev-guide.md   — codebase guide: architecture, state, functions,
                      localStorage, i18n, FX convention, PDF generation
CLAUDE.md           — project briefing for Claude Code sessions
2026-05-24 18:25:19 +00:00

113 lines
4.7 KiB
Markdown

# CLAUDE.md — Freelance Invoicing App
## Project overview
Single-file browser app for generating freelance invoices as PDF or print preview. No backend, no build step. All app logic lives in `app/index.html` (~1600 lines: HTML + embedded CSS + embedded JS). Runtime config is loaded from `app/config.yml` via `fetch()`.
## Running locally
Must be served over HTTP — `fetch()` for `config.yml` fails on `file://`.
```
npx serve app # recommended
python3 -m http.server 8000 # run from app/
```
No `package.json`, no bundler, no install step.
## Architecture
```
app/
index.html — entire application
config.yml — runtime config (recipients, products, translations, etc.)
docs/
devdocs/Specs.md
user-guide.md / admin-guide.md / dev-guide.md
```
**CDN dependencies** (loaded via `<script>` tags in `index.html`):
- `js-yaml 4.1.0` (cdnjs) — parses `config.yml`
- `jsPDF 2.5.1` (cdnjs) — PDF generation
**Boot sequence:** `loadCfg()``boot()``buildLangBar()`, `buildForm()`, `restoreStorage()`, `loadLines() || addLine()`
**Key globals:**
- `cfg` — parsed config object
- `lang` — active language code
- `lid` / `lines` — line ID counter + map of active invoice line IDs
- `tlid` / `tLines` — tax-line ID counter + map
- `_loading` — bool; `true` while `loadLines()` restores from localStorage; blocks `saveLines()` writes
**localStorage keys:**
- `inv_data_v1` — form field values (all `[data-ls]` elements)
- `inv_lines_v1` — serialised line array
- `inv_generated_v1` — bump flag; invoice number increments on next load after Generate
- `zoomIdx` — font-size index
**PDF / preview:** `gatherData()` feeds both `buildPreviewHTML()` (overlay) and `buildPDF()` (file download). Uses jsPDF with Helvetica, mm units; paper size from config (`a4` or `letter`).
**i18n:** `t(key)` looks up `cfg.translations[key][lang]`, falls back to `default-code`. `relabel()` re-applies all translatable labels on language switch.
## Critical state & invariants
Do not break these behaviours:
- **Charge-to:** value `""` → all CT fields locked. Predefined entry → fill + lock. `"other"` → unlock.
- **Line description:** value `""` → QTY/UOM/Price disabled. Predefined → UOM locked, Price editable. `"other"` → all three editable.
- **FX:** FX Yes → Price locked, calculated as `per_item_foreign / exchange_rate`. FX No → Price re-enabled.
- **`_loading` guard:** `saveLines()` must no-op when `_loading === true`; set `_loading = true` for the entire duration of `loadLines()`.
- **`addLine()` must `return i`** — `loadLines()` depends on the returned ID.
- **`saveStorage()`** is triggered by `change` events on all `[data-ls]` elements; do not remove that listener.
- **Invoice number bump:** occurs on the next page load after Generate via the `inv_generated_v1` flag; do not bump on Generate itself.
- **FX rate convention:** X foreign units = 1 local unit. `price_local = price_foreign / exchange_rate`.
## Config structure (`config.yml`)
| Section | Purpose |
|---|---|
| `default-code` / `languages` | Available UI languages and default |
| `hide-payment-info` | Bool; omits payment block from PDF |
| `date-format` / `paper-format` | Output formatting |
| `tax-types` | Tax rows available in the tax table |
| `uom` | Units of measure for line items |
| `charge-to` | Recipients; each may have `project-codes` and `currency` |
| `project-codes` | Global fallback project code list |
| `products` | Predefined line items; multilingual `description`/`uom`/`price` |
| `currencies` | Currency codes and exchange rates |
| `translations` | All UI strings keyed by language code |
Config changes (new recipients, products, translations) **never require JS changes**.
## Common tasks
### Add a persisted form field
1. Add the element to `buildForm()` with a `data-ls="your_key"` attribute.
2. Nothing else — `saveStorage()` and `restoreStorage()` handle it automatically.
3. If the label is translatable, add the key to `cfg.translations` in `config.yml` and update `relabel()` to set it.
### Add a new line-level field
1. Add the input to the line row HTML in `addLine()`.
2. Add the field to the object written in `saveLines()`.
3. Read it back in `loadLines()` when reconstructing each line.
4. If translatable, update `relabel()`.
### Add a new language
1. Add the language code to `languages` in `config.yml`.
2. Add a translation entry for every key in `cfg.translations` using the new code.
3. Set `default-code` to the new code if it should be the default.
4. No JS changes needed; `buildLangBar()` and `t()` pick it up automatically.
### Add a new product
Add an entry under `products` in `config.yml`:
```yaml
products:
- id: my_product
description:
en: "Widget"
fr: "Gadget"
uom:
en: "ea"
price: 99.00
```
No JS changes needed.