# Dynamic field image — developer guide

This document explains how to add an **image** stored as a **dynamic field** (JSON on the model), how it appears in **edit forms**, and how it is shown on the **read-only record view** (e.g. company profile tabs).

## Overview

- Configuration lives in the `dynamic_fields` table (`LncShared\App\Models\DynamicField`).
- Values are stored in the model’s JSON column (for `Company`, usually `data` on `DynamicModel`).
- On save, the file is processed by `LncShared\App\Helpers\Image`, written under `storage/app/img/`, and the **URL** returned by `route('media.img', …)` is stored in JSON.
- The SPA sends a **base64 data URL** from the cropper (or an existing **URL** string), not a classic multipart file — validation must allow that (see below).

## 1. Create the dynamic field (admin)

Use **Dynamic Fields** in the UI (if enabled) or insert a row in `dynamic_fields`.

| Concept | Typical value |
|--------|----------------|
| **model** | `App\Models\Company` (or another `DynamicModel`) |
| **field_type** | `image` |
| **field_name** | Snake case API key, e.g. `demo_image` |
| **display_name** | Label shown in forms and profile |
| **target_json** | Usually `data` (default when empty: model’s `defaultJsonTarget`) |
| **order** | Controls which **profile tab** lists the field (see §4) |
| **image_dimension** (meta) | Optional. `200` = square crop; `400*300` = width×height (min 100 per side). Drives Croppa size and server resize. |

**Universe (optional):** meta `universe` can restrict the field to companies whose `universe` matches.

## 2. Server-side behaviour

### Storage and URL

- `lncshared/app/Models/DynamicModel.php` — `setDynamicFieldValue()` for `field_type === 'image'` calls `Image::save($value, '', $dynamicField->image_dimension)`.
- `lncshared/app/Helpers/Image.php` — Intervention Image; saves JPEG under `storage/app/img/{uniqid}.jpg`; returns the public URL for `media.img`.
- `lncshared/routes/web.php` — `GET media/img/{filename}` → `StorageController@image` serves the file.

### Validation

- Default rules for dynamic images come from `DynamicField::getValidationRuleAttribute()` (`lncshared/app/Models/DynamicField.php`).
- Laravel’s built-in `image` rule only accepts **uploaded files**. JSON/API and the Vue cropper send **strings** (data URL or existing URL), so the project uses **`LncShared\App\Rules\DynamicFieldImagePayload`** instead of `nullable|image` for dynamic field type `image`.

Do **not** override the dynamic field’s custom `validation` meta with `nullable|image` unless you only use multipart file uploads.

### Company-specific overrides

- `app/Models/Company.php` — `Company::rules()` merges base rules with `dynamicRules()`; IM/TKO overrides do not apply to arbitrary image field names.

## 3. Edit form — capturing the image

### Shared input wiring

- `lncshared/resources/assets/js/components/fields/inputs-list.vue`
  - If `field.input_type === 'image'` **and** the API provides `image_width` / `image_height` (from `image_dimension` in `DynamicResource::dynamicFieldsData()`), the **Croppa** component is used.
  - Otherwise it falls back to a plain file input (`input-text` with `type="file"`).

### Cropper component

- `lncshared/resources/assets/js/components/forms/input-image.vue` — outputs a **JPEG data URL** (`generateDataUrl('image/jpeg', 0.8)`), which the backend accepts via `DynamicFieldImagePayload` and `Image::save()`.

### Example embed

Company **edit** uses `company-form` → `inputs-list` with the resource-shaped `company` object from `CompanyResource` (meta per field: `value`, `type`, `input_type`, `order`, etc.).

## 4. Profile / record view — showing the image

### Tab placement (Company)

`resources/assets/js/components/models/companies/profile.vue` splits fields by **`order`**:

| Order range | Tab |
|-------------|-----|
| 0–99 | Overview |
| 100–199 | Administrative |
| 200–299 | **Other** |
| … | (adjust as needed) |

Set the dynamic field’s **order** (e.g. `210`) so it appears under **Other**.

### Read-only list

- `lncshared/resources/assets/js/components/fields/fields-list.vue` — for `field.type === 'image'`, renders an `<img :src="field.value">` (URL stored in JSON).
- The Vue filter `dynamic-field` in `lncshared/resources/assets/js/extension.js` — for `type === 'image'`, returns HTML `<img>` when the filter is used with **`v-html`**; `fields-list` uses a dedicated `<img>` branch so URLs are not double-escaped.

## 5. Native `logo` vs dynamic image (Company)

- **`logo`** is a **native** column on `companies`, not a dynamic field. It uses the `image` cast on `BaseModel` and `Company` meta (`image_dimension`) for the same `Image::save` pipeline.
- **Dynamic image fields** use **`data` JSON** keys and `DynamicField` rows — follow §1–4 above.

## 6. Checklist

1. Create `dynamic_fields` row: `model`, `field_type = image`, `field_name`, `display_name`, `order` for the right tab, optional `image_dimension`.
2. Ensure **`image_dimension`** is set if you want Croppa + `input-image` (not only the file fallback).
3. Avoid custom validation `nullable|image` on that dynamic field for JSON/API saves.
4. Rebuild front-end assets after JS changes (`npm run dev` / `npm run prod`).
5. Confirm `storage/app/img` is writable and `media/img/{file}` is reachable for the stored URL.

## 7. Main file reference

| Area | Path |
|------|------|
| Dynamic field model & validation | `lncshared/app/Models/DynamicField.php` |
| Image payload rule | `lncshared/app/Rules/DynamicFieldImagePayload.php` |
| JSON write + `Image::save` | `lncshared/app/Models/DynamicModel.php` |
| Image helper | `lncshared/app/Helpers/Image.php` |
| API field metadata | `lncshared/app/Http/Resources/DynamicResource.php` |
| Edit inputs | `lncshared/.../fields/inputs-list.vue`, `.../forms/input-image.vue` |
| Read-only list | `lncshared/.../fields/fields-list.vue` |
| Filter | `lncshared/resources/assets/js/extension.js` |
| Company profile tabs | `resources/assets/js/components/models/companies/profile.vue` |
