# Working on MDX

Everything runs in Docker; there is no PHP or Node on the host. Prefix commands:
`docker compose exec php php bin/console …`, `docker compose exec php php bin/phpunit`.
The `node` service rebuilds the UI on every save — check `docker compose logs node` instead of running a build.

Read `README.md` first: it has the architecture, the multi-tenancy rules and the API reference.

## Tables: the house style

**Every table in the UI follows this. Read it before adding or changing one.** The components that enforce it
live in `assets/react/components/ui.jsx`; a table that does not use them is a table that will drift.

### 1. One actions column, always last, always called "Acciones"

Everything the user can click in a row goes in it — nothing clickable is left loose in a data column.
`ListView` adds the header itself, so a page lists only its data columns:

```jsx
<ListView
    columns={[t('visits.when'), t('visits.person')]}   // no actions column, no status column
    renderRow={(visit) => (
        <Row key={visit.id} status={visit.status} label={t(`visits.status.${visit.status}`)}>
            …
            <Actions>…</Actions>
        </Row>
    )}
/>
```

A table with no actions at all passes `actions={false}` rather than leaving an empty header.

### 2. Buttons look like buttons, and their colour says what they do

Every action is an outlined button in the colour of its kind of action — the same colour in every table and
modal, so two different actions next to each other never share one (`ACTIONS` in `ui.jsx`):

| Action | Colour | Icon | Worded examples |
|---|---|---|---|
| `confirm` | green | `check` | "Marcar realizada", "Marcar como atendida", "Renovar", "Descargar recibo" (of a confirmed payment) |
| `danger` | red | `ban`, `close` | "Quitar", reject, cancel, disable |
| `edit` | blue | `pencil` | "Cambiar respuesta" |
| `open` | violet | `eye` | "Revisar", "Ver unidades", "Mostrar" |
| `file` | teal | `file`, `paperclip`, `download` | "Requisitos", "Abrir" |
| `setup` | indigo | `receipt`, `plus`, `calendar` | "Horario de visitas", "Invitar", uploads, "Agregar" |
| `revert` | amber | `undo` | "No asistió", "Reactivar", "Pasar a revisión" |

- **Icon button** (`<IconButton icon="pencil" label={t('common.edit')} />`) for actions an icon carries on its
  own. It takes its colour from the icon; never pass a variant. `label` is both the tooltip and the accessible
  name — never omit it.
- **Worded button** (`<ActionButton action="setup">`) for anything else. If you cannot name the icon that means
  the action, use words. A router `Link` that acts as one uses `className={actionClass('open')}`.
- Never `variant="secondary"` or `variant="link"` for an action: they read as links. `variant="link"` is only for
  text that is a link (a file name).
- Do not invent a second icon or colour for a meaning that already has one. A new kind of action gets a row in
  this table and in `ACTIONS` first.

### 3. No status column: the row colour is the status

`<Row status={item.status} label={t(`…status.${item.status}`)}>` tints the row via `toneFor()`. Tables have no
"Estado" column and no status badge in a cell.

- `label` is required with `status`: it becomes the row's tooltip and hidden text read with the first cell, so
  colour is never the only signal. Put `<RowLegend statuses={…} />` above every tinted table, listing every value
  it can show (including `inactive` when disabled rows can appear).
- Add every new status value to `TONES` in `ui.jsx`, and keep the values of one table on different tones. One
  left out shows neutral, which reads as "no status".
- `muted` greys out a disabled row; pair it with `status="inactive"`. Data with no status (a settings list) passes
  no `status` and stays plain.
- Extra information that used to sit under a status badge (a rejection reason, "rented until…") moves into the
  cell it describes. A status the user changes in place (Mantenimiento) is a dropdown in the actions cell.
- When every row shares one status (a list already filtered to "active"), colour by the field that actually
  varies — the dashboard tints by the renewal answer, not by the contract status.

### 4. A search box and at least one dropdown above every table

```jsx
<FilterBar
    search={list.filters.q}
    onSearch={(q) => list.update({ q })}
    searchPlaceholder={t('visits.searchPlaceholder')}
    filters={[{ name: 'status', label: t('common.status'), value: …, onChange: …, options: […] }]}
/>
```

- The placeholder names the fields the box actually searches ("Buscar por nombre, correo o unidad…"), so the
  API and the placeholder have to be changed together.
- Filters are labelled dropdowns, not tabs and not bare checkboxes. `Tabs` stays for switching between
  *different* tables or time ranges, never for filtering one:
  - a page's own views use `<Tabs variant="page" id=…>` as a tab bar right under the page header, with the
    content (filters first, then the table) inside `<TabPanel>` — see Servicios públicos;
  - a range inside a section keeps the default pill control (the dashboard's 30/60/90 días).
- Every list endpoint takes `?q=`. Repositories add it with `self::whereTerm($qb, $term, [...])`
  (`LandlordOwnedRepository`), which escapes `%` and `_` so a wildcard in the box is matched literally.
  `tests/Functional/Api/ListSearchTest.php` covers each list; add yours to its provider.

### 5. After changing a table, check it renders

A missing import or constant in a page component is a blank screen, not a build error — webpack compiles it and
React throws at render. So don't stop at "compiled successfully": open the page, and open the modals it owns.
`grep -oE '<[A-Z][A-Za-z]+' page.jsx` against the file's imports catches the usual cause in a second.

### 6. Amounts

Every money input is a `MoneyField` (with its label) or an `AmountInput` (bare, e.g. one per unit in a list), from
`components/MoneyField.jsx`. Never a plain `<input>` for an amount: they group thousands as the landlord types, in the
landlord's locale. Convert with `parseAmountInput()` on submit. Readings, percentages and areas are not money.

### 7. Strings

Every visible string goes through `t()` in `lib/i18n.js` — including button labels, dropdown options and
placeholders. API error codes are translated by code, so backend messages stay in English.
