mirror of
https://github.com/kbenestad/invoice.git
synced 2026-06-18 08:04:32 +00:00
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
113 lines
4.7 KiB
Markdown
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.
|