# Invoice PDF Customization — Implementation Plan

**Spec:** docs/specs/2026-05-31-invoice-pdf-customization.md  ·  **Slug:** invoice-pdf-customization  ·  **Wave:** 11
**Depends on:** foundation-auth-rbac, invoice-settings-page, invoices-core

## Goal
Let tenant admins customize how invoice PDFs (HTML print documents) look: pick one of three fixed layouts (`classic` / `modern` / `minimal`), toggle the optional `project` and `sku` line-item columns, choose a date format, and override the accent color. Settings live on the existing `tenant_settings` row and are consumed at invoice-render time by `invoices-core`'s `renderInvoiceHtml`. The feature ships a new admin sub-route `/settings/invoicing/pdf-template` with a split-panel live preview plus a downloadable sample.

## Architecture
- **Storage:** five new columns on the existing per-tenant `tenant_settings` table (the same Drizzle table object `tenantSettings` that `invoice-settings-page` extends in `packages/db/src/schema/tenant-settings.ts`). No new table.
- **Read/write helpers:** new `getInvoicePdfTemplate` / `updateInvoicePdfTemplate` in `packages/db`, scoped with `tenantQuery` (the same scoping the upstream `getInvoiceSettings`/`updateInvoiceSettings` helpers use). They do NOT reuse the AI package's `getTenantSettings`/`upsertTenantSettings`.
- **API (`apps/zync-api`):** three Hono routes under `/api/settings/invoicing/pdf-template` (GET, PATCH, GET `/preview`). All admin-guarded via `authMiddleware` + `requirePermission('settings:write'|'settings:read')`, Zod-validated, no raw Drizzle in routes.
- **Render integration:** `invoices-core`'s `renderInvoiceHtml` (in `apps/zync-api/src/invoices/render.ts`, template in `html-template.ts`) gains a `pdfTemplate: InvoicePdfTemplate` arg. Layout name selects the page-frame variant, column flags gate the optional `<th>/<td>` cells, date format drives an `Intl`-or-`date-fns` formatter, accent hex is injected as a CSS custom property (`--invoice-accent`) in the print `<style>` block. The `/preview` endpoint reuses `renderInvoiceHtml` with hard-coded sample invoice/customer data.
- **UI (`apps/zync-app`):** new `PdfTemplatePage.tsx` rendered as a tab inside the invoice settings area, registered in `settingsNav.ts` + `router.tsx`. Split panel: controls left, live client-side HTML preview right (a React mirror of the print template), plus a "Download sample PDF" button that opens the `/preview` endpoint in a new tab for browser print.
- **Upstream exports consumed:** `tenantSettings` (Drizzle table), `Db`, `tenantQuery`, `authMiddleware`, `requirePermission`, `serializeInvoice`/`InvoiceObject` shape, `renderInvoiceHtml`, `Customer`/`CustomerObject`, `useDirection`, `toast`, `@zync/ui` primitives (`Card`, `Radio`, `Checkbox`, `Input`, `Button`, `Stack`, `Divider`, `Form`, `FormField`, `FormLabel`, `FormError`), `SettingsShell`/`SETTINGS_NAV` (from invoice-settings-page wiring).

## Tech Stack
- **DB:** `packages/db` (Drizzle over Neon Postgres via Hyperdrive); raw SQL migration under `apps/zync-api/migrations`.
- **Types:** `packages/types` (shared `InvoicePdfTemplate` type + Zod schema source of truth).
- **API:** `apps/zync-api` (Hono on Cloudflare Workers). Bindings used: `DB` (Hyperdrive→Neon), `STORAGE` (R2 — only indirectly, via the existing render path; preview is generated live, not snapshotted). Zod validation; CSP-preserving (no inline event handlers; print CSS in a `<style>` block).
- **UI:** `apps/zync-app` (Vite + React), `@zync/ui` primitives, `@tanstack/react-query`, `react-hook-form`, `useDirection` for RTL.

## Wave Plan
| Sub-wave | Tasks | Files touched | Parallelizable? |
|----------|-------|---------------|-----------------|
| 11a — schema + types | 1 (migration + Drizzle delta), 2 (shared type + Zod schema) | `apps/zync-api/migrations`, `packages/db/src/schema/tenant-settings.ts`, `packages/types` | Task 2 parallel with Task 1 |
| 11b — query + API | 3 (query helpers), 4 (GET/PATCH routes), 5 (preview route) | `packages/db/src/queries/invoice-pdf-template.ts`, `apps/zync-api/src/routes/settings/invoicing/pdf-template.ts` | 4 and 5 parallel after 3 |
| 11c — render integration | 6 (extend `renderInvoiceHtml` with template props) | `apps/zync-api/src/invoices/render.ts`, `html-template.ts` | After 3 (needs the type) |
| 11d — UI | 7 (data hook), 8 (page + live preview), 9 (nav + router wiring) | `apps/zync-app/src/features/settings/hooks`, `apps/zync-app/src/features/settings/pages`, `settingsNav.ts`, `router.tsx` | 8 after 7; 9 after 8 |

## Tasks

### Task 1: Migration + Drizzle delta — PDF template columns on `tenant_settings`
**Blocks:** 3, 6  ·  **Blocked by:** —
**Files:**
- Create: `apps/zync-api/migrations/0XXX_tenant_settings_invoice_pdf_template.sql`
- Modify: `packages/db/src/schema/tenant-settings.ts` (add the five columns to the existing `tenantSettings` Drizzle table object — do NOT create a new table)
**Steps:**
- [ ] Write the raw SQL `ALTER TABLE tenant_settings ADD COLUMN ...` migration with the five columns and their CHECK constraints exactly as the spec defines (idempotent guards `IF NOT EXISTS` where the project's migration convention allows).
- [ ] Add the matching Drizzle columns to the existing `tenantSettings` table definition so reads/writes are typed. Keep snake_case DB column names; Drizzle property names follow the file's existing convention.
- [ ] Number the migration after the `invoice-settings-page` invoice-defaults migration (it also alters `tenant_settings`); confirm ordering in the migrations directory.
**Schema / Interfaces:**
```sql
ALTER TABLE tenant_settings
  ADD COLUMN invoice_pdf_layout TEXT NOT NULL DEFAULT 'classic'
    CHECK (invoice_pdf_layout IN ('classic', 'modern', 'minimal')),
  ADD COLUMN invoice_pdf_show_project BOOLEAN NOT NULL DEFAULT false,
  ADD COLUMN invoice_pdf_show_sku BOOLEAN NOT NULL DEFAULT false,
  ADD COLUMN invoice_pdf_date_format TEXT NOT NULL DEFAULT 'dd.MM.yyyy'
    CHECK (invoice_pdf_date_format IN ('dd.MM.yyyy', 'yyyy-MM-dd', 'MMMM d, yyyy')),
  ADD COLUMN invoice_pdf_accent_hex TEXT;  -- null = use system default accent
```
**Acceptance:**
- [ ] Migration applies cleanly on a Neon branch; `tenant_settings` gains the five columns with correct defaults and CHECK constraints.
- [ ] `invoice_pdf_accent_hex` is nullable; the other four are `NOT NULL` with the spec defaults.
- [ ] Existing `tenant_settings` rows backfill to defaults (`classic`, both flags false, `dd.MM.yyyy`, null accent) without error.

### Task 2: Shared `InvoicePdfTemplate` type + Zod schema
**Blocks:** 3, 4, 6, 7  ·  **Blocked by:** —
**Files:**
- Create: `packages/types/src/invoice-pdf-template.ts`
- Modify: `packages/types/src/index.ts` (re-export the type and schema)
**Steps:**
- [ ] Define the `InvoicePdfTemplate` TypeScript type and the `updateInvoicePdfTemplateSchema` Zod object (the source of truth for the PATCH body). Use literal unions matching the DB CHECKs.
- [ ] `invoice_pdf_accent_hex` is `string | null`; validate as a 7-char `#RRGGBB` hex (regex `^#[0-9a-fA-F]{6}$`) OR `null`. All PATCH fields optional (partial patch).
- [ ] Export `INVOICE_PDF_TEMPLATE_DEFAULTS` so the query helper and UI share one default source.
- [ ] Do NOT reuse any AI-package type names; keep this type distinct from `AISettings`/`ai_tenant_settings`.
**Schema / Interfaces:**
```typescript
export type InvoicePdfLayout = 'classic' | 'modern' | 'minimal';
export type InvoicePdfDateFormat = 'dd.MM.yyyy' | 'yyyy-MM-dd' | 'MMMM d, yyyy';

export interface InvoicePdfTemplate {
  invoice_pdf_layout: InvoicePdfLayout;
  invoice_pdf_show_project: boolean;
  invoice_pdf_show_sku: boolean;
  invoice_pdf_date_format: InvoicePdfDateFormat;
  invoice_pdf_accent_hex: string | null; // #RRGGBB or null = system default
}

export const INVOICE_PDF_TEMPLATE_DEFAULTS: InvoicePdfTemplate = {
  invoice_pdf_layout: 'classic',
  invoice_pdf_show_project: false,
  invoice_pdf_show_sku: false,
  invoice_pdf_date_format: 'dd.MM.yyyy',
  invoice_pdf_accent_hex: null,
};

// zod
export const updateInvoicePdfTemplateSchema = z.object({
  invoice_pdf_layout: z.enum(['classic', 'modern', 'minimal']).optional(),
  invoice_pdf_show_project: z.boolean().optional(),
  invoice_pdf_show_sku: z.boolean().optional(),
  invoice_pdf_date_format: z.enum(['dd.MM.yyyy', 'yyyy-MM-dd', 'MMMM d, yyyy']).optional(),
  invoice_pdf_accent_hex: z.string().regex(/^#[0-9a-fA-F]{6}$/).nullable().optional(),
});
export type UpdateInvoicePdfTemplate = z.infer<typeof updateInvoicePdfTemplateSchema>;
```
**Acceptance:**
- [ ] `InvoicePdfTemplate`, `updateInvoicePdfTemplateSchema`, and `INVOICE_PDF_TEMPLATE_DEFAULTS` are importable from `@zync/types`.
- [ ] Schema rejects a non-`#RRGGBB` accent and accepts `null`; accepts a valid partial patch with any subset of fields.

### Task 3: `getInvoicePdfTemplate` / `updateInvoicePdfTemplate` query helpers
**Blocks:** 4, 5  ·  **Blocked by:** 1, 2
**Files:**
- Create: `packages/db/src/queries/invoice-pdf-template.ts`
- Modify: `packages/db/src/index.ts` (export both helpers)
**Steps:**
- [ ] `getInvoicePdfTemplate(db, tenantId)`: select the five `invoice_pdf_*` columns from `tenant_settings` for the tenant via `tenantQuery`; if no row exists yet, return `INVOICE_PDF_TEMPLATE_DEFAULTS`.
- [ ] `updateInvoicePdfTemplate(db, tenantId, patch)`: upsert (`INSERT ... ON CONFLICT (tenant_id) DO UPDATE`) only the columns present in `patch`; set `updated_at = now()`. Return the fresh merged `InvoicePdfTemplate`.
- [ ] Use `tenantQuery` scoping so the write is constrained to the caller's tenant; do NOT reuse the AI package's `getTenantSettings`/`upsertTenantSettings`.
- [ ] Normalize accent hex to lowercase on write for stable comparison/preview.
**Schema / Interfaces:**
```typescript
export function getInvoicePdfTemplate(db: Db, tenantId: string): Promise<InvoicePdfTemplate>;
export function updateInvoicePdfTemplate(
  db: Db, tenantId: string, patch: UpdateInvoicePdfTemplate,
): Promise<InvoicePdfTemplate>;
```
**Acceptance:**
- [ ] First-ever PATCH for a tenant inserts the `tenant_settings` row (ON CONFLICT path); subsequent PATCH updates in place and bumps `updated_at`.
- [ ] A tenant with no row reads back `INVOICE_PDF_TEMPLATE_DEFAULTS`.
- [ ] No import of `getTenantSettings`/`upsertTenantSettings` from the AI package; all access is `tenantQuery`-scoped.

### Task 4: `GET` / `PATCH /api/settings/invoicing/pdf-template` routes
**Blocks:** 7  ·  **Blocked by:** 3
**Files:**
- Create: `apps/zync-api/src/routes/settings/invoicing/pdf-template.ts`
- Modify: `apps/zync-api/src/app.ts` (mount the route group under `/api/settings/invoicing/pdf-template`)
**Steps:**
- [ ] `GET /api/settings/invoicing/pdf-template`: `requirePermission('settings:read')`; return `getInvoicePdfTemplate(db, tenantId)`.
- [ ] `PATCH /api/settings/invoicing/pdf-template`: `requirePermission('settings:write')`; validate body with `updateInvoicePdfTemplateSchema` (422 on failure with field errors); call `updateInvoicePdfTemplate`; return the updated `InvoicePdfTemplate`.
- [ ] Mount behind `authMiddleware`; tenant id derived from the session, never from the body.
- [ ] Preserve cross-cutting requirements: rely on the app-wide CSP/security-headers middleware (do not weaken it); validate ALL input via Zod (`require-zod-validation-in-routes`); no raw Drizzle from the route — go through Task 3 helpers (`no-raw-drizzle-from-routes`).
**Schema / Interfaces:**
```
GET   /api/settings/invoicing/pdf-template  → 200 InvoicePdfTemplate                 (requires settings:read)
PATCH /api/settings/invoicing/pdf-template  → 200 InvoicePdfTemplate | 422 {errors}  (requires settings:write)
  body: Partial<{ invoice_pdf_layout, invoice_pdf_show_project, invoice_pdf_show_sku,
                  invoice_pdf_date_format, invoice_pdf_accent_hex }>
```
**Acceptance:**
- [ ] Non-admin (lacking `settings:write`) receives 403 on PATCH; lacking `settings:read` receives 403 on GET.
- [ ] Invalid accent hex or unknown layout value yields 422 with a field-level error; a valid partial body updates only named fields.
- [ ] Route uses query helpers, not inline Drizzle; body is Zod-validated.

### Task 5: `GET /api/settings/invoicing/pdf-template/preview` — sample PDF (HTML print doc)
**Blocks:** 8  ·  **Blocked by:** 3, 6
**Files:**
- Create: `apps/zync-api/src/routes/settings/invoicing/pdf-template-preview.ts` (or add the handler to the Task 4 file — same route group)
- Create: `apps/zync-api/src/invoices/sample-invoice.ts` (deterministic sample invoice + lines + customer + tenant business stub)
**Steps:**
- [ ] `GET /api/settings/invoicing/pdf-template/preview`: `requirePermission('settings:read')`; load the tenant's current `getInvoicePdfTemplate(db, tenantId)` (so the sample reflects unsaved-but-saved settings) and the tenant business identity; build deterministic sample invoice data (the spec's "Acme Corp / Dana Cohen" example: two lines Design ₪2K and Dev ₪8K, VAT ₪1.7K, total ₪11.7K, INV-0001, issue 01.06.2026, due 30.06.2026).
- [ ] Render via the extended `renderInvoiceHtml` (Task 6) passing the loaded `pdfTemplate`; serve `Content-Type: text/html`, `Content-Disposition: inline; filename="sample-invoice.html"` so the browser print dialog produces the PDF. This sample is NOT a real invoice and is never snapshotted to R2.
- [ ] Preserve CSP: print CSS in a `<style>` block, no inline event handlers; reuse the same Heebo `@font-face` embed path as the real renderer for Hebrew tenants.
**Schema / Interfaces:**
```
GET /api/settings/invoicing/pdf-template/preview → 200 text/html (sample print document)   (requires settings:read)
```
**Acceptance:**
- [ ] Endpoint returns a valid HTML print document reflecting the tenant's saved layout/columns/date-format/accent.
- [ ] Hebrew tenant preview renders `dir="rtl" lang="he"` with Heebo embedded; English tenant renders `dir="ltr" lang="en"`.
- [ ] Response is never written to R2 and contains no real invoice data.

### Task 6: Extend `renderInvoiceHtml` to honor PDF template settings
**Blocks:** 5, 8  ·  **Blocked by:** 2
**Files:**
- Modify: `apps/zync-api/src/invoices/render.ts` (add `pdfTemplate` to the `renderInvoiceHtml` args; thread it to the template)
- Modify: `apps/zync-api/src/invoices/html-template.ts` (layout variants, optional columns, accent CSS var, date formatter)
- Modify: `apps/zync-api/src/routes/invoices/html.ts` (load `getInvoicePdfTemplate` and pass it through; spec 15's HTML route + issue-tax snapshot path both feed the template)
**Steps:**
- [ ] Extend the `renderInvoiceHtml` argument object with `pdfTemplate: InvoicePdfTemplate`. Callers (`html.ts` live render, `transitions.ts` issue-tax snapshot, the preview route) load it via `getInvoicePdfTemplate(db, tenantId)` and pass it in.
- [ ] **Layout:** `invoice_pdf_layout` selects a page-frame variant in `html-template.ts`:
  - `classic` — logo top-left, business identity top-right, large "INVOICE" heading, bill-to below, full-width line table, totals bottom-right, footer bottom-center.
  - `modern` — business identity panel right-aligned, bold accent stripe across the top, left-aligned bill-to.
  - `minimal` — no logo section, monochrome (accent color ignored), compact header.
- [ ] **Columns:** always render Description, Quantity, Unit price, Tax rate, Amount. Render the `Project` column header+cells only when `invoice_pdf_show_project=true`; render the `SKU / item code` column only when `invoice_pdf_show_sku=true`. Keep logical column order (description first); RTL `dir="rtl"` on the table handles visual reversal for Hebrew (unchanged from invoices-core).
- [ ] **Date format:** format issue/due/tax dates using `invoice_pdf_date_format` — map `dd.MM.yyyy`, `yyyy-MM-dd`, `MMMM d, yyyy` to a formatter (date-fns format tokens, or an `Intl.DateTimeFormat` equivalent) applied to ALL dates in the document. This overrides the default `dateStyle:'long'` formatting from invoices-core for the in-document dates.
- [ ] **Accent:** inject `:root { --invoice-accent: <hex>; }` into the print `<style>` block, where `<hex>` = `invoice_pdf_accent_hex ?? '#006b6b'` (system default teal). Use `var(--invoice-accent)` for the table header row, dividers, total-row background, and page-header stripe. For `minimal` layout, force monochrome (do not emit the accent var / use neutral tokens) per spec. Hex (not OKLCH) is used because PDF/print renderers don't reliably support OKLCH custom properties.
- [ ] Preserve all invoices-core invariants: no Browser Rendering binding, no puppeteer; Heebo `@font-face` embed for Hebrew; IL legal fields (business name, ח.פ./ע.מ., sequential number, VAT rate, total inc. VAT) remain present regardless of column toggles.
**Schema / Interfaces:**
```ts
// extended signature (was: { invoice, lines, tenant, customer, locale })
export function renderInvoiceHtml(args: {
  invoice: InvoiceRow;
  lines: InvoiceLineRow[];
  tenant: TenantBilling;
  customer: CustomerObject;
  locale: 'he-IL' | 'en-US';
  pdfTemplate: InvoicePdfTemplate;   // NEW
}): string;
```
**Acceptance:**
- [ ] `classic`/`modern`/`minimal` produce visibly distinct page frames; `minimal` omits the logo and renders monochrome (no accent).
- [ ] `Project`/`SKU` columns appear only when their flags are true; the five core columns always appear.
- [ ] All document dates render in the selected `invoice_pdf_date_format`; accent hex drives the table header / divider / total-row / header-stripe styling for `classic`/`modern`.
- [ ] Hebrew RTL output, Heebo embed, and IL legal fields are unchanged from invoices-core; no Browser Rendering binding referenced.

### Task 7: `useInvoicePdfTemplate` data hook
**Blocks:** 8  ·  **Blocked by:** 4
**Files:**
- Create: `apps/zync-app/src/features/settings/hooks/useInvoicePdfTemplate.ts`
**Steps:**
- [ ] `useInvoicePdfTemplate()`: react-query `useQuery(['settings','invoicing','pdf-template'])` → `GET /api/settings/invoicing/pdf-template`.
- [ ] `useUpdateInvoicePdfTemplate()`: `useMutation` → `PATCH /api/settings/invoicing/pdf-template` with an `UpdateInvoicePdfTemplate` payload; on success invalidate the query and `toast.success`; on 422 surface field errors to the form; on other errors `toast.error`.
- [ ] Expose the current `InvoicePdfTemplate` (defaulting to `INVOICE_PDF_TEMPLATE_DEFAULTS` while loading) so the live preview can render before the first save.
**Acceptance:**
- [ ] Mutation invalidates the settings query; the page reflects saved values without a full reload.
- [ ] A 422 maps server field errors back onto the form fields.

### Task 8: `PdfTemplatePage` — split-panel settings + live preview
**Blocks:** 9  ·  **Blocked by:** 6, 7
**Files:**
- Create: `apps/zync-app/src/features/settings/pages/PdfTemplatePage.tsx`
- Create: `apps/zync-app/src/features/settings/components/InvoicePdfPreview.tsx` (client-side React mirror of the print template)
**Steps:**
- [ ] Render inside `SettingsShell` with breadcrumb "Settings > Invoicing > PDF Template". Two-panel layout: left = settings form (single `react-hook-form` instance), right = live preview + "Download sample PDF" button.
- [ ] **Layout control:** Radio group (`role="radiogroup"`) — "Classic (recommended)" / "Modern (header right)" / "Minimal (no logo)" bound to `invoice_pdf_layout`.
- [ ] **Columns control:** Checkboxes for the two optional columns — `invoice_pdf_show_project` ("Project"), `invoice_pdf_show_sku` ("SKU / item code"). The five default columns (Description, Quantity, Unit price, Tax rate per line, Amount) are shown as always-on (disabled/checked) for clarity.
- [ ] **Accent color control:** hex Input (default-display `#006b6b` swatch) + native color picker; empty/cleared = "use system default" → submits `invoice_pdf_accent_hex: null`. Disable/grey this control when layout is `minimal` (monochrome).
- [ ] **Date format control:** Radio group — "DD.MM.YYYY (Israeli)" → `dd.MM.yyyy`, "YYYY-MM-DD (ISO)" → `yyyy-MM-dd`, "Month D, YYYY" → `MMMM d, yyyy`.
- [ ] **[Save template]** button: submits the full form via `useUpdateInvoicePdfTemplate()`.
- [ ] **Live preview:** `InvoicePdfPreview` re-renders client-side on every form change using the current (possibly unsaved) form values + the spec's sample data (Acme Corp / Dana Cohen, INV-0001, two lines, VAT, total). It mirrors layout/columns/date-format/accent so feedback is instant. Accent applied via a scoped `--invoice-accent` CSS variable (not global tokens).
- [ ] **Download sample PDF:** opens `GET /api/settings/invoicing/pdf-template/preview` in a new tab (server render → browser print → save as PDF) for a final fidelity check.
- [ ] A11y + i18n: every control has an associated `<FormLabel htmlFor>`; Radio groups use `role="radiogroup"`; the live preview region is non-focusable decorative content with an `aria-label`; validation errors announced via `aria-describedby`/`FormError`; respect `prefers-reduced-motion` for any preview transition; layout is RTL-aware via `useDirection` so Hebrew renders right-to-left and the preview swaps panel order.
- [ ] Use only `@zync/ui` primitives (Card, Form, FormField, FormLabel, FormError, Input, Radio, Checkbox, Button, Stack, Divider) — no raw HTML form controls (`no-raw-html-in-pages`); no hardcoded colors/spacing — use tokens (`no-hardcoded-colors`, `no-hardcoded-spacing`); the accent swatch/preview is the one allowed user-driven color and uses the scoped CSS variable, not a hardcoded token.
**Acceptance:**
- [ ] Changing any control updates the right-panel preview instantly (no save round-trip); `minimal` hides the logo and disables the accent control.
- [ ] Save persists all fields; reload shows the saved template; "Download sample PDF" opens a print-ready HTML document matching the preview.
- [ ] Page renders correctly RTL with translated labels; all controls keyboard-operable and labelled (axe: no violations); reduced-motion honored.

### Task 9: Register `/settings/invoicing/pdf-template` tab in nav + router
**Blocks:** —  ·  **Blocked by:** 8
**Files:**
- Modify: `apps/zync-app/src/features/settings/settingsNav.ts` (add the PDF Template sub-entry/tab under Invoicing)
- Modify: `apps/zync-app/src/router.tsx` (register route `/settings/invoicing/pdf-template` → `PdfTemplatePage`, admin-guarded)
**Steps:**
- [ ] Add the PDF Template tab under the existing Invoicing settings entry (sub-route of `/settings/invoicing`), `requiresPermission: 'settings:write'`, so it appears as a tab within the invoice settings page (spec 125) and in the SettingsShell sidebar.
- [ ] Register the route lazily in `router.tsx`, guarded so non-admins are redirected/forbidden, consistent with other `/settings/*` admin pages.
- [ ] Provide Hebrew + English translation keys for the tab/nav label ("PDF Template" / "תבנית PDF") and all control labels used in Task 8.
**Acceptance:**
- [ ] `/settings/invoicing/pdf-template` appears as a tab/sidebar entry for admins and is hidden/forbidden for users lacking `settings:write`.
- [ ] Direct navigation to the route renders `PdfTemplatePage` inside `SettingsShell` under the Invoicing section.
- [ ] Nav and control labels are translated for both `he` and `en`.
