# Input & Image Standards — 360LM PWAs

> **Owner:** Harish / Pramod  
> **Status:** Live — Custodian PWA (v11, 2026-06-17)  
> **Scope:** Any 360LM PWA that handles money amounts or requires proof images  
> **Claude instruction:** Before implementing either feature in any PWA, **ask the user to confirm** that the standard spec applies, or whether any setting needs to be adjusted for that app's context. Do not auto-implement silently.

---

## 1. Amount Input — Indian Number Formatting

### What it is
All money/amount input fields in 360LM PWAs display values in the **Indian number system** as the user types:

```
??,??,???.??
e.g.  1,23,45,678.50  (not 12,345,678.50)
```

Grouping rule: last 3 digits form the first group, then groups of 2 moving left. Up to 2 decimal places.

### When to apply
- **Default: YES** for any PWA that includes a monetary amount input (`transfer amount`, `advance`, `receipt`, `salary`, `expense`, `invoice value`, etc.)
- **Ask user to confirm** when scoping a new PWA — this is a personal preference and may not suit every context (e.g. a purely English-audience report tool)

### Canonical implementation

**HTML — use `type="text"` with `inputmode="decimal"`** (NOT `type="number"`):
```html
<input type="text" inputmode="decimal" id="tf-amount"
       placeholder="0" autocomplete="off"
       oninput="fmtAmountInput(this); onTfAmountChange()">
```
Rationale: `type="number"` does not support comma characters, so formatting is impossible inside the field.

> **Exception:** Line-item rows that use `querySelectorAll('input[type="number"]')` to auto-calculate totals should remain `type="number"`. Do not change those.

**Three utility functions (copy verbatim, no modification needed):**

```javascript
function fmtAmountInput(el) {
  const raw = el.value, pos = el.selectionStart;
  const beforeCursor = raw.slice(0, pos).replace(/,/g, '');
  let clean = raw.replace(/[^0-9.]/g, '');
  const dot = clean.indexOf('.');
  if (dot !== -1)
    clean = clean.slice(0, dot + 1) + clean.slice(dot + 1).replace(/\./g, '').slice(0, 2);
  const [intPart, decPart] = clean.split('.');
  const formatted = _indFmt(intPart || '') + (decPart !== undefined ? '.' + decPart : '');
  el.value = formatted;
  if (el.setSelectionRange) {
    let digits = 0, newPos = formatted.length;
    for (let i = 0; i < formatted.length; i++) {
      if (formatted[i] !== ',') {
        digits++;
        if (digits === beforeCursor.length) { newPos = i + 1; break; }
      }
    }
    if (beforeCursor.length === 0) newPos = 0;
    el.setSelectionRange(newPos, newPos);
  }
}

function _indFmt(n) {
  if (!n || n.length <= 3) return n;
  const last3 = n.slice(-3);
  let rest = n.slice(0, -3);
  const groups = [];
  while (rest.length > 0) { groups.unshift(rest.slice(-2)); rest = rest.slice(0, -2); }
  return groups.join(',') + ',' + last3;
}

function _parseAmt(elOrValue) {
  const v = typeof elOrValue === 'string' ? elOrValue : elOrValue.value;
  return parseFloat(v.replace(/,/g, '')) || 0;
}
```

**Replace every `parseFloat(el.value)` or `Number(el.value)` on a formatted field** with `_parseAmt(el)`.

**Auto-fill / pre-fill from DB:** after setting `.value`, call `fmtAmountInput(el)` immediately:
```javascript
amtEl.value = row.amount;
fmtAmountInput(amtEl);
```

**Cursor position:** the `fmtAmountInput` function preserves cursor position correctly during mid-number editing by counting non-comma characters before the cursor and restoring to the same digit-count position in the formatted string.

---

## 2. Image Capture — Proof, Receipt & Verification

### What it is
A two-phase flow for any image that needs to be attached to a transaction as proof:

1. **Capture phase** — one of:
   - File picker / camera (for receipt/document photos) — supports JPG and PNG, with `capture="environment"` for rear camera on mobile
   - CTH (Cash Hand-to-Hand) flow — front camera captures receiver's face with 3-2-1 countdown; then receiver signs on a canvas; composite image (photo + "RECEIVER SIGNATURE" label + signature) is created at 800 px wide

2. **Edit phase** — fullscreen image editor opens BEFORE compression is applied, with:
   - **Crop** (orange dashed selection with corner handles; darkens outside; "Apply Crop" button; undoable)
   - **Rectangle** annotation
   - **Oval** annotation
   - **Line** annotation
   - **Pen** (freehand) annotation
   - 5 color swatches: Red `#ef4444` · Black `#111827` · Blue `#2563eb` · Green `#16a34a` · Orange `#f97316`
   - 3 stroke weights: Thin (2 px) · Medium (4 px) · Thick (7 px)
   - Undo (up to 20 levels; undo of crop correctly restores original canvas dimensions)

3. **Compression phase** — after editing, image is JPEG-compressed with a quality slider (10–95, default 70), max size 100 KB enforced. The compressed base64 is what gets stored in DB.

### Storage
- Column: `receipt_image TEXT` (nullable base64 JPEG) on each transaction table
- **Never include `receipt_image` in list/ledger queries** — it bloats every row by ~100 KB. Instead: show a 📎 icon on rows that have an image, and lazy-fetch the image only on tap via a separate REST call.

### When to apply
- **`cash_with_receipt` payment mode** — mandatory image (file pick / camera), optional receipt number field
- **`cash_hand_to_hand` payment mode** — mandatory CTH composite (front camera + signature)
- **Other payment modes** — image is optional (file pick may be offered but not required)
- **Ask user to confirm** which modes need mandatory vs optional image, and whether CTH applies

### Confirmation prompt for Claude
Before implementing in a new PWA, ask:

> "This feature uses the standard 360LM image capture spec (file pick + crop/annotate editor + JPEG compression up to 100 KB). Do you want:
> (a) **Full editor** — crop + rectangle + oval + line + pen + color + stroke width + undo [standard]
> (b) **Crop only** — simpler UI, no annotation tools
> (c) **No editor** — compress immediately after capture, no editing step
>
> Which payment/submission modes require a mandatory image? Which are optional?"

### Key implementation details

**Canvas size:** scale image to max 800 px on the longest side before opening the editor. This keeps the compressed output manageable.

**CTH countdown:** 3-2-1 overlaid on the `<video>` element using `setInterval`. The video uses `transform:scaleX(-1)` for a mirror preview; the canvas capture un-mirrors with `ctx.translate(w,0); ctx.scale(-1,1)`.

**Desktop fallback for CTH:** if `getUserMedia({video:{facingMode:'user'}})` fails (desktop without webcam or permission denied), skip the camera step and proceed to signature-only mode. The composite is then label + signature without a photo pane.

**Undo architecture — canvas + dimensions:**
```javascript
// Saves BOTH imageData AND canvas dimensions so crop undo restores to original size
_edUndoStack.push({
  imageData: ctx.getImageData(0, 0, edC.width, edC.height),
  width:  edC.width,
  height: edC.height
});
```

**Size estimate (base64 → bytes):**
```javascript
const header    = b64.indexOf(',') + 1;
const sizeBytes = Math.round((b64.length - header) * 3 / 4);
```

**Lightbox for viewing stored images:** fetch `receipt_image` on demand, open in a fullscreen overlay (`z-index: 800`). The editor sits at `z-index: 950`.

---

## 3. Change History

| Date       | Feature             | PWA       | Notes |
|------------|---------------------|-----------|-------|
| 2026-06-17 | Indian amount input | Custodian | Added to `#tf-amount`, `#edit-amount`, `#recon-amount` |
| 2026-06-17 | Image editor (full) | Custodian | Crop + Rect + Oval + Line + Pen; CTH composite flow |
| 2026-06-17 | Cash With Receipt   | Custodian | New payment mode; mandatory proof image |
| 2026-06-17 | Cash Hand-to-Hand   | Custodian | New payment mode; CTH front-camera + signature composite |
