# CLAUDE.md - LogShip ERP Nuxt Frontend

## Project Overview

**LogShip ERP** is an enterprise resource planning web application built with **Nuxt 4** and **Vue 3 Composition API**. It connects to an **iDempiere** ERP backend via REST API. The app also integrates with Strapi (CMS/media), Laravel, PostgREST, Elasticsearch, DHL, Sendcloud, Shopify, and PlentyOne.

- **App Name:** LogShip WMS / ERP Solutions
- **Company:** LogYou
- **Production URL:** https://app.logship.de
- **Dev URL:** https://dev-app.logship.de

---

## Tech Stack

| Layer | Technology |
|-------|-----------|
| Framework | Nuxt 4.4+ (Vue 3.5+, Composition API, `<script setup>`); source lives under `app/` (Nuxt 4 srcDir) |
| UI | Bulma CSS (primary), Nuxt UI 4 (Pro merged into `@nuxt/ui` — no separate `@nuxt/ui-pro`), alga-css (PostCSS) |
| Data Tables | AG Grid Enterprise (v35.2.1 — license-locked, see AG Grid Usage; server-side row model) |
| Charts | AmCharts 5 |
| Custom UI | tedir-select (SelectBox), tedir-calendar (DatePicker), tedir-dropzone (AttachBox) |
| State | Nuxt `useState()` composables (NO Pinia/Vuex) |
| Auth | Cookie-based JWT tokens (Bearer auth to iDempiere) |
| Mobile | Capacitor 8 (Android) |
| PDF | pdfmake, jsPDF, pdf-lib |
| Search | Elasticsearch 8 |
| i18n | Custom composable (`useTranslation`) with en-US, de-DE, es-ES |
| Deployment | PM2 cluster mode (2 instances, port 3001) |

---

## Directory Structure

**Nuxt 4 structure:** all application source lives under `app/` (the Nuxt 4 default `srcDir`).
The `~`/`@` aliases resolve to `app/`; `~~`/`@@` resolve to the repo root. `server/`, `public/`,
`data/`, `scripts/` and config files stay at the repo root.

```
/
├── app/                      # srcDir (Nuxt 4) — everything below resolves via ~/ and @/
│   ├── pages/                # File-based routing (469 files, CRUD per entity)
│   ├── components/           # Vue components (524 files, organized by domain)
│   │   ├── admin/           # Admin forms/actions (UserForm, CountryForm, etc.)
│   │   ├── materials/       # Product/material management
│   │   ├── sales/           # Orders, invoices, shipments
│   │   ├── procurements/    # Purchase orders, receipts
│   │   ├── partners/        # Business partner management
│   │   ├── accountings/     # Accounting entities
│   │   ├── manufacturings/  # Production & BOM
│   │   ├── settings/        # System config forms
│   │   ├── fulfillment/     # Fulfillment-specific
│   │   ├── requests/        # Request/ticket management
│   │   ├── rmas/            # Return management
│   │   ├── assets/          # Asset management
│   │   ├── projects/        # Project management
│   │   ├── AgGrid/          # Custom AG Grid cell renderers
│   │   ├── BI/              # Business intelligence visuals
│   │   ├── Charts/          # Chart components
│   │   └── [Root]           # Shared: NavBar, SideBar, ComboBox, AgGrid, modals, etc.
│   ├── composables/          # State & logic composables (37+ files)
│   │   ├── states.ts        # All global UI state (useState)
│   │   ├── menu/admin/      # Admin menu definitions
│   │   ├── menu/fulfillmentCustomer/ # Customer menu definitions
│   │   └── use*.ts          # Utility composables
│   ├── layouts/              # app, admin, auth, mobile, org, tracking, new-dash
│   ├── middleware/           # auth.ts, guest.ts, org.ts
│   ├── plugins/              # Service worker, push notifications, amcharts
│   ├── assets/
│   │   ├── css/             # fonts, style, custom, main, sidebar, theme
│   │   ├── sass/            # main.scss
│   │   ├── langs/           # shared.js, menus.js, navs.js (translations)
│   │   └── alga/            # alga-css modules
│   ├── router.options.ts    # Custom router config (Nuxt-conventional location)
│   ├── forms/               # Custom route-definition helpers (imported by router.options.ts)
│   ├── windows/             # Route-wrapper component for /windows, /create, /edit, ...
│   ├── app.vue              # Root: <UApp><NuxtPage :keepalive="{ max: 10 }" /></UApp>
│   └── app.config.ts        # ui: { colors: { primary: 'blue', neutral: 'slate' } }
├── server/                   # NOT under app/ — stays at root
│   ├── api/                 # Server API routes (1177 files, 50+ modules)
│   ├── middleware/           # Server middleware (auth, fetch)
│   └── utils/               # Helpers: fetchHelper, refreshToken, DHL, Strapi, etc.
├── public/                   # Static files, flags, PWA icons, sw-notifications.js (served at root URL — reference as /assets/..., NOT /public/assets/...)
├── data/                     # Local SQLite (push subscriptions), runtime data
├── nuxt.config.ts
├── reference/                # Tracked-but-not-built reference material, incl. recipes.md (see below)
└── tsconfig.json
```

### Sibling repo: `../logship` (ERP-linked marketing sites + product docs)

The marketing/landing sites and the Docusaurus manual that used to live here under
`docs/` and `doc-saurus/` were **extracted to an independent sibling repo `../logship`**
(own git, own per-site hosting) to keep this ERP project fast — the real bloat was
`doc-saurus/`'s ~354 MB `node_modules` being watched/indexed. That content is
**AI-authored from this codebase**: open `../logship` as its own project and the ERP is
auto-attached as a read-only root via its `.claude/settings.local.json` (no
`--add-dir` needed; nothing is copied/synced, the ERP is read live). See
`../logship/CLAUDE.md`. The ERP's own `public/assets/...` are unaffected (the sites
carry their own asset copies).

> Each project is its own sibling repo under the workspace root, like this one:
> `../logship` (ERP-linked), plus `../jet-charter` and `../it-beratung` (independent
> client sites, no ERP link) and `../amazon-sp-assessment` (moved out of this repo's
> root). Only `../logship` has the ERP attached.

### `reference/` folder (tracked; excluded from Nuxt/Vite watching)

`reference/` holds reference / dev / ops-staging material that is **tracked in git** but
**not consumed by the build or runtime**, and is kept out of the dev server's
file-watching (`.nuxtignore` + `nuxt.config.ts` watch-ignore) so it never slows the app.
Contents: `dpd/` (DPD API reference), `ci/` (production deploy scripts — copied into
Jenkins manually, not run from the repo), `test-scripts/` (manual dev/diagnostic tools).
See `reference/README.md`. Build-critical scripts stay OUT of here — e.g.
`scripts/generate-icons.js`, which `npm run build` runs via `generate:icons`.

---

## Architecture Patterns

### Server API Route Pattern (the standard)

Every server API route follows this **try/catch with token refresh** pattern:

```typescript
// server/api/[domain]/[entity]/index.get.ts
const handleFunc = async (event: any, authToken: string | null = null) => {
  const token = authToken ?? await getTokenHelper(event)
  const organizationId = getCookie(event, 'logship_organization_id')
  const query = getQuery(event)

  const res = await fetchHelper(event, 'models/c_entity?$filter=AD_Org_ID eq ' + organizationId, 'GET', token, null)
  return res
}

export default defineEventHandler(async (event) => {
  let data: any = {}
  try {
    data = await handleFunc(event)
  } catch(err: any) {
    try {
      let authToken = await refreshTokenHelper(event)
      data = await handleFunc(event, authToken)
    } catch(error: any) {
      data = errorHandlingHelper(err?.data ?? err, error?.data ?? error)
    }
  }
  return data
})
```

**Key server utilities:**
- `fetchHelper(event, url, method, token, body)` — wraps `$fetch` to iDempiere API (`config.api.url + '/' + url`)
- `refreshTokenHelper(event)` — two-tier: tries `auth/refresh`, falls back to `auth/tokens` with stored credentials
- `getTokenHelper(event)` — reads `logship_it` cookie
- `errorHandlingHelper(err, error)` — formats error responses
- `forceLogoutHelper(event, error)` — logs out + redirects on auth failure

### iDempiere API URL Structure

The backend uses OData-style queries:
```
models/{table_name}?$filter={condition}&$orderby={field} asc&$top={limit}&$skip={offset}&$select={fields}&$expand={relations}
```

**Common table names (iDempiere models):**
- `c_bpartner` — Business Partners
- `c_bpartner_location` — Partner Locations
- `ad_org` — Organizations
- `ad_user` — Users
- `m_product` — Products
- `m_product_category` — Product Categories
- `c_order` — Orders (Sales + Purchase)
- `c_orderline` — Order Lines
- `c_invoice` — Invoices
- `c_invoiceline` — Invoice Lines
- `m_inout` — Shipments/Receipts (Material Movements)
- `m_inoutline` — Shipment Lines
- `c_payment` — Payments
- `m_warehouse` — Warehouses
- `m_locator` — Locators (storage locations)
- `m_pricelist` — Price Lists
- `m_pricelist_version` — Price List Versions
- `c_currency` — Currencies
- `c_country` — Countries
- `c_tax` — Tax Rates
- `c_tax_category` — Tax Categories
- `c_doctype` — Document Types
- `c_charge` — Charges
- `c_paymentterm` — Payment Terms
- `ad_role` — Roles
- `ad_client` — Tenants/Clients

### Generic Filter Endpoint

```
GET /api/filters/{model}/{encoded_odata_filter}
```
Used to query any iDempiere model with OData filters. Example:
```typescript
const res = await $fetch(`/api/filters/c_bpartner/$filter=AD_Org_ID eq ${orgId} and IsActive eq true`, {
  headers: useRequestHeaders(['cookie'])
})
```

---

## Cookie System (Authentication & Context)

| Cookie | Purpose | Format |
|--------|---------|--------|
| `logship_it` | Access token (JWT) | Bearer token string |
| `logship_rt` | Refresh token | Token string |
| `logship_xu` | Username | Base64 encoded |
| `logship_py` | Password | Base64 encoded |
| `logship_session` | Session ID | String |
| `logship_client_id` | Tenant/Client ID | Number |
| `logship_role_id` | Role ID | Number |
| `logship_organization_id` | Current Org ID | Number |
| `logship_warehouse_id` | Current Warehouse ID | Number |
| `logship_user_id` | User ID | Number |
| `logship_user` | Full user object | JSON object |
| `logship_organization` | Full org object | JSON object |
| `logship_role` | Full role object | JSON object |
| `logship_client` | Full client object | JSON object |
| `logship_language` | Language (iDempiere fmt) | e.g. `en_US` |

**Client-side access:**
```typescript
const organizationId = useCookie('logship_organization_id')
const user = useCookie('logship_user')
```

**Server-side access:**
```typescript
const organizationId = getCookie(event, 'logship_organization_id')
const token = getCookie(event, 'logship_it')
```

---

## Key API Endpoints by Entity

Routes live under `server/api/<module>/` and mirror the file tree — discover them
with `ls server/api/<module>/` rather than maintaining a catalog here. Modules
include `partners`, `materials`, `orders`, `invoices`, `inouts`, `organizations`,
`settings`, `admin`, `dashboard`, `bi`, `fulfillment`, `commission`, `chat` (50+).

Conventions that repeat across modules:
- **CRUD:** `index.get` (list), `[id].get` (one), `store.post`, `update.put`, `destroy.delete` (body `{ ids: [...] }`)
- **Dropdowns:** `…/select`. **AG Grid server rows:** `POST …/ag-grid/<name>` (body = `IServerSideGetRowsRequest`, returns `{ rows, lastRow }`)
- **Documents:** `POST …/[id]/document-action` (`{ docAction: 'CO' | 'VO' | 'CL' }` — Complete / Void / Close), `…/[id]/print` (PDF), `…/[id]/send-email`
- **Lines / sub-resources:** `…/[id]/order-line`, `…/[id]/inout-line`, `…/[id]/packages`, `…/[id]/locations`, …
- **Real-time:** `…/stream` (SSE)
- **Ad-hoc queries against any model:** the generic filter endpoint (see Architecture Patterns above).

---

## Page CRUD Pattern

Every entity follows this file structure:

```
pages/[domain]/[entity]/
├── index.vue          — List page (AG Grid table)
├── create.vue         — Create page (renders Form component)
└── [id]/
    ├── view.vue       — Read-only detail view
    ├── edit.vue       — Editable detail view
    ├── duplicate.vue  — Copy record
    └── print.vue      — Print view (if applicable)
```

### Index Page Pattern (List)

```vue
<script setup>
definePageMeta({ middleware: ['auth', 'org'] })

const { data, pending, error, refresh } = await useFetch('/api/[domain]/[entity]', {
  headers: useRequestHeaders(['cookie'])
})

// Map backend PascalCase to frontend camelCase
const rowData = computed(() => data.value?.records?.map(item => ({
  id: item.id,
  name: item.Name,
  description: item.Description,
  isActive: item.IsActive,
  // Map nested objects: item.AD_Org_ID?.identifier → organization
})) || [])

const colDefs = ref([
  { width: 50, checkboxSelection: true, headerCheckboxSelection: true },
  { headerName: 'Name', field: 'name', cellComponent: 'AgCustomLink' },
  // ...more columns
])

const alert = ref({ status: '', message: '' })

// Delete handler
const deleteRecords = async (selectedIds) => {
  const res = await $fetch('/api/[domain]/[entity]/destroy', {
    method: 'DELETE',
    headers: useRequestHeaders(['cookie']),
    body: { ids: selectedIds }
  })
}
</script>
```

### Create Page Pattern

```vue
<script setup>
definePageMeta({ middleware: ['auth', 'org'] })

const form = ref({
  id: '', name: '', description: '', isActive: true,
  processing: false,
  alert: { status: '', message: '', processing: false }
})

const save = async (record) => {
  const res = await $fetch('/api/[domain]/[entity]/store', {
    method: 'POST',
    headers: useRequestHeaders(['cookie']),
    body: { /* mapped fields */ }
  })
  if (Number(res.status) === 200) {
    navigateTo('/[domain]/[entity]')
  } else {
    form.value.alert = { status: res.status, message: res.message }
    form.value.processing = false
  }
}
</script>

<template>
  <DomainEntityForm v-model="form" @save="save" />
</template>
```

---

## Component Patterns

### Form Component Pattern

**Naming:** `components/[domain]/[Entity]Form.vue`

```vue
<script setup>
import SelectBox from 'tedir-select'

const props = defineProps({ modelValue: Object })
const emit = defineEmits(['update:modelValue', 'save'])

const form = ref({
  ...props.modelValue,
  processing: false,
  alert: { status: '', message: '', processing: false }
})

// Load dropdown options
const loading = ref({})
const getOptions = ref({})
const setOptions = async (url, key) => {
  loading.value[key] = true
  const res = await $fetch('/api/' + url, { headers: useRequestHeaders(['cookie']) })
  if (res) getOptions.value[key] = res
  loading.value[key] = false
}

// Save handler
const save = () => {
  form.value.processing = true
  emit('save', form.value, props.modelValue.id)
}

// Watch for parent changes
watch(() => props.modelValue, (val) => {
  Object.assign(form.value, val)
})
</script>

<template>
  <div>
    <AlertBox :alert="form.alert" />
    <!-- Form fields -->
    <div class="field">
      <label class="label">Name</label>
      <input class="input" v-model="form.name" />
    </div>
    <SelectBox v-model="form.currencyId" :options="getOptions.currencies" />
    <button class="button is-primary" @click="save" :disabled="form.processing">
      Save
    </button>
  </div>
</template>
```

### Action Component Pattern (AG Grid context menu)

**Naming:** `components/[domain]/[Entity]Action.vue`

```vue
<script setup>
const props = defineProps({ params: Object })
const tabs = useTabs()

const viewHandler = () => {
  tabs.value.push({ link: '/[domain]/[entity]/' + props.params.data.id + '/view', text: 'View' })
  navigateTo('/[domain]/[entity]/' + props.params.data.id + '/view')
}
</script>
```

### Detail Record Tab Pattern

**Naming:** `components/[domain]/[Entity]DetailRecord.vue`

Manages sub-tabs for complex entity views (e.g., Partner → Locations, Bank Accounts, Users).

---

## State Management

**NO Pinia/Vuex.** All state uses Nuxt's `useState()` in `composables/states.ts`:

```typescript
export const useSideBar = () => useState<boolean>('sideBar', () => false)
export const useTabs = () => useState<any[]>('tabs', () => [{ link: '/', text: 'Home', active: true }])
export const usePrintPreview = () => useState<boolean>('printPreview', () => false)
export const usePrintParam = () => useState<any>('printParam', () => {})
// ... 33 total state composables
```

**State persistence strategy:**
- **Cookies:** Auth data, user, org, role, language (persistent, cross-tab)
- **localStorage:** Theme (`logship_theme`), language (`logship_lang`), tabs, scanner preference
- **sessionStorage:** Detail record active tab
- **useState:** UI runtime state (sidebar, modals, active menus)

---

## Data Fetching

### Client-side (pages/components)

```typescript
// SSR-safe initial data load
const { data, pending, error, refresh } = await useFetch('/api/endpoint', {
  headers: useRequestHeaders(['cookie'])  // ALWAYS include cookies for auth
})

// Client-side mutations
const res = await $fetch('/api/endpoint', {
  method: 'POST',
  headers: useRequestHeaders(['cookie']),
  body: { /* data */ }
})
```

### Server-side (API routes)

```typescript
// Using fetchHelper (for iDempiere)
const res = await fetchHelper(event, 'models/c_bpartner?$filter=...', 'GET', token, null)

// Using event.context.fetch (middleware-provided, auto-handles cookies)
const res = await event.context.fetch('models/c_bpartner?$filter=...', 'GET', token, null)

// Other backend helpers
const res = await postgrestHelper(event, '/rpc/function_name', 'POST', body)
const res = await strapiHelper(event, 'endpoint', 'GET', null)
const res = await laravelHelper(event, 'api/endpoint', 'POST', body)
const res = await elasticHelper(event, 'index/_search', 'POST', body)
```

---

## Layouts

| Layout | Usage | Components |
|--------|-------|-----------|
| `app` | Default authenticated pages | NavBar + SideBar + TabBar + Footer |
| `admin` | Admin panel | NavBarAdmin + SideBarAdmin |
| `org` | Organization selection | NavBarOrg + SideBarOrg |
| `auth` / `new-auth` | Login/register pages | Centered form |
| `mobile` | Mobile routes (`/mobile/*`) | Portrait-locked, landscape warning |
| `tracking` | Public tracking page | Minimal layout |
| `new-dash` | New dashboard | TopMegaBar + NewSideBar + MainNavigationMenu |

**Usage in pages:**
```typescript
definePageMeta({ layout: 'admin' })
```

---

## Middleware

- **`auth.ts`** — Checks `/api/idempiere-auth/authenticated`; redirects to `/signin` if not authenticated. Mobile pages fall back to cookie check on network errors.
- **`guest.ts`** — Redirects authenticated users away from signin/register pages.
- **`org.ts`** — Organization context check (currently disabled/commented out).

**Page usage:**
```typescript
definePageMeta({ middleware: ['auth', 'org'] })
```

---

## Styling Conventions

- **Primary CSS:** Bulma classes (`field`, `label`, `input`, `button is-primary`, `columns`, `column`)
- **Theme variables:** `--theme-bg-primary`, `--theme-bg-secondary`, `--theme-text-primary`, `--theme-border`, etc.
- **Dark mode:** CSS variables switch values; classes `.dark` / `[data-mode="dark"]`
- **Scoped styles:** Use `<style scoped>` with alga-css `@use` directives
- **Icons:** MDI icons via Nuxt UI (`@iconify-json/mdi`)
- **No Tailwind** — uses Bulma + custom CSS + alga-css

---

## i18n / Localization

```typescript
const { t, isGerman, isEnglish } = useTranslation()
// t.value.fieldName returns translated string
```

- Translation files: `assets/langs/shared.js`, `assets/langs/menus.js`, `assets/langs/navs.js`
- Supported: `en-US`, `de-DE`, `es-ES`
- Cookie format: `logship_language` = `en_US` (iDempiere format, underscore)
- localStorage format: `logship_lang` = `en-US` (browser format, dash)

---

## AG Grid Usage

> ⚠️ **Version is license-locked — do NOT upgrade.** `ag-grid-enterprise` and
> `ag-grid-vue3` are pinned to **exactly `35.2.1`** (no caret) in `package.json`.
> 35.2.1 is the **latest version the current AG Grid Enterprise license key permits**;
> a newer AG Grid would refuse the key (license-invalid watermark/console errors).
> Leave both at 35.2.1 and keep the versions identical. Only bump them if the license
> is extended to cover a later version — and if so, bump both packages together to the
> same version. (Routine `npm update`/dependency-upgrade passes must skip AG Grid.)

### Client-side AG Grid
```vue
<AgGrid :rowData="rowData" :columnDefs="colDefs" />
```

### Server-side AG Grid
```vue
<AgGridServer
  :columns="colDefs"
  sourceEndpoint="orders/ag-grid/so"
  :rowClassRules="rowClassRules"
/>
```

The server endpoint receives AG Grid's `IServerSideGetRowsRequest` as POST body and returns `{ rows, lastRow }`.

### Custom Cell Renderers
Located in `components/AgGrid/[EntityType]/`:
- `AgCustomLink.vue` — Clickable link to detail page
- `AgCustomDateOrdered.vue` — Formatted date
- `AgAddress.vue` — Address with indicators
- `AgQtyTotal.vue` — Calculated quantities
- `AgShipStatus.vue` — Status badges

---

## Step-by-Step: Creating a New Feature/Entity

Combines patterns already shown above — no new concepts:
1. **Server routes** — `server/api/[domain]/[entity]/{index.get,[id].get,store.post,update.put,destroy.delete}.ts`, each using the `handleFunc` + try/catch/refreshToken pattern (see Architecture Patterns).
2. **Form component** — `components/[domain]/[Entity]Form.vue`: accept `modelValue`, emit `save`, local `ref` with `processing` + `alert`, `SelectBox` for dropdowns, Bulma layout (see Component Patterns).
3. **Action component** (optional, AG Grid menu) — `components/[domain]/[Entity]Action.vue`.
4. **Pages** — `pages/[domain]/[entity]/{index,create,[id]/view,[id]/edit}.vue` (see Page CRUD Pattern).
5. **Menu entry** — add to `composables/menu/admin/[module].ts`.
6. **Translations** — add keys to `assets/langs/shared.js` for en-US / de-DE / es-ES.

---

## Key Conventions

1. **Always include cookie headers** in client-side fetches: `headers: useRequestHeaders(['cookie'])`
2. **Backend field mapping:** iDempiere returns PascalCase (`Name`, `IsActive`, `AD_Org_ID`); map to camelCase in frontend
3. **Nested ID fields:** iDempiere returns `{ id: 123, identifier: "Name", model-name: "table" }` for foreign keys; extract `.id` or `.identifier`
4. **Page middleware:** Always use `definePageMeta({ middleware: ['auth', 'org'] })` for authenticated pages
5. **Form alert pattern:** Every form has `alert: { status: '', message: '', processing: false }` for error display
6. **Status check:** API success = `Number(res.status) === 200`
7. **Navigation:** Use `useTabs()` to add tabs when navigating to detail pages
8. **No Pinia** — use `useState()` composables in `composables/states.ts` for any new global state
9. **SelectBox component:** Import from `tedir-select` for all dropdown/select fields
10. **File naming:** Pages and components follow lowercase-kebab for directories, PascalCase for component files

---

## External Integrations

| Service | Helper | Base URL Config |
|---------|--------|-----------------|
| iDempiere (main ERP) | `fetchHelper.ts` | `URLV1` → `config.api.url` |
| Strapi (CMS/media) | `strapiHelper.ts` | `STRAPIV1` → `config.api.strapi` |
| Laravel | `laravelHelper.ts` | `LARAVEL` → `config.api.laravel` |
| PostgREST | `postgrestHelper.ts` | `POSTGREST` → `config.api.postgrest` |
| Elasticsearch | `elasticHelper.ts` | `ELASTIC` → `config.api.elastic` |
| DHL Shipping | `dhlHelper.ts`, `dhlBaseHelper.ts`, `dhlCustomHelper.ts` | `DHLURL` → `config.api.dhlurl` |
| Sendcloud | `sendcloudHelper.ts` | `SENDCLOUD` → `config.api.sendcloud` |
| Shopify | `shopifyNewAuthHelper.ts` | GraphQL API 2025-10 |
| PlentyOne | Direct API calls | Via server routes |

---

## Runtime Config Keys

**Private (server-only):** `config.api.url`, `config.api.strapi`, `config.api.postgrest`, `config.api.laravel`, `config.api.dhlurl`, `config.api.elastic`, etc.

**Public (client-accessible):** `config.public.strapi`, `config.public.aggrid`, `config.public.vapidPublicKey`, `config.public.doctypeid`, `config.public.pricelistid`, `config.public.countryid`, `config.public.partnergroupid`, `config.public.solutionname`

---

## Common Recipes

Living reference for non-obvious patterns (org/bpartner resolution, billing rates,
iDempiere flag/casing gotchas, the staff chat module, commissioning→camera, DHL
CN23, Nuxt 4 build/heap issues, lazy list pages, `useLookup`, …). Kept in
`reference/recipes.md` and imported below so they load into context — **add new
recipes there, not here**, to keep this file under its size limit.

@reference/recipes.md
