# Render & thay đổi Layer Options (TeeInBlue)

Tài liệu này hướng dẫn cách mobile app render các option control và xử lý thao tác khi user chạm vào item, nhập text, upload ảnh, hoặc toggle visibility. Đọc trước [customization-teeinblue.md](./customization-teeinblue.md) để hiểu khái niệm tổng quát về snapshot, template, design layer.

## Nguồn dữ liệu duy nhất: `snapshot.layerOptions`

Sau khi khởi tạo service, lấy snapshot:

```ts
const service = WM.initCustomizationTeeinblue(campaignData, options);
let snapshot = service.getSnapshot();
```

`snapshot.layerOptions` là một **mảng đã được filter sẵn** chỉ chứa các layer mà user có thể tương tác:

- Layer `visible == true`.
- Đã thỏa dependency (`isShowLayer` trả về true).
- Không phải `form_type == 'static'` hoặc `form_type == 'linked'`.
- Không phải `type == 'group'`.
- Có ít nhất một trong `form_label`, `form_type`, `additional_option`.

→ App KHÔNG cần tự lọc lại. Cứ render mọi item trong `layerOptions` theo thứ tự.

## Phân loại theo `input_type`

Service tự normalize `input_type` để app render đúng widget. Đừng dựa vào `layer.type` hay `layer.form_type` thô vì có logic ưu tiên không tầm thường.

| `input_type` | Widget mobile | Method update giá trị |
| --- | --- | --- |
| `text` | Text input một dòng | `changeInputValue(layer, value)` |
| `photo` | Nút "Upload photo" → file picker → upload → URL | `changeUploadValue(layer, imageUrl, { file })` |
| `option` | Grid/list các option (text hoặc icon) | `changeLayerOptionValue(item, layer)` |
| `clipart` | Grid ảnh clipart | `changeLayerOptionValue(item, layer)` |
| `grouped_clipart` | 2 cấp: chọn group → chọn item trong group | `changeLayerOptionValue(childItem, group, layer)` |

Switch render theo `input_type`:

```ts
function renderLayerOption(layer) {
  switch (layer.input_type) {
    case 'text':              return <TextOption layer={layer} />;
    case 'photo':             return <PhotoOption layer={layer} />;
    case 'option':            return <OptionList layer={layer} />;
    case 'clipart':           return <ClipartGrid layer={layer} />;
    case 'grouped_clipart':   return <GroupedClipartPicker layer={layer} />;
    default:                  return null; // input_type khác chưa hỗ trợ
  }
}
```

## Chuẩn refresh sau mỗi thao tác

Tất cả method update đều trả về snapshot mới. **App phải dùng snapshot mới để re-render**, không giữ lại reference layer cũ:

```ts
function applyChange(nextSnapshot) {
  snapshot = nextSnapshot;
  renderUI(snapshot);
}
```

Lý do: mỗi `change*` đều rebuild dependency, active state, linked layer, design layer, validation, fonts. Nếu app giữ lại reference cũ, sẽ thấy state lệch.

Mọi method trả về snapshot mới có cùng shape với `getSnapshot()`. Nếu cần lấy lại bất kỳ lúc nào (ví dụ trước add to cart): `service.getSnapshot()`.

## 1. Text input

### Field cần dùng
| Field | Ý nghĩa |
| --- | --- |
| `layer.id` | Định danh layer, dùng khi gọi method. |
| `layer.form_label` | Label hiển thị phía trên input. |
| `layer.value` | Giá trị hiện tại (string). |
| `layer.max_length` | Giới hạn ký tự, nếu có. |
| `layer.required` | Bắt buộc hay không. |
| `layer.form_input_textcase` | `"uppercase"` → service tự uppercase khi render preview, nhưng app vẫn nên hiển thị giá trị y nguyên user nhập (UX gõ chữ thường hợp lý hơn). |

### UI mẫu

```tsx
<View>
  <Text>{layer.form_label}{layer.required && ' *'}</Text>
  <TextInput
    value={layer.value || ''}
    maxLength={layer.max_length || undefined}
    onChangeText={(text) => {
      applyChange(service.changeInputValue(layer, text));
    }}
    placeholder={layer.placeholder || layer.text || ''}
  />
</View>
```

### Lưu ý

- Gọi `changeInputValue` mỗi keystroke là OK, service rebuild rất nhẹ (không có canvas).
- Service tự apply `prefix` + `suffix` + uppercase khi tạo `designLayers`. App KHÔNG cần làm thủ công.
- Linked layer (`linked == layer.id`) cũng được update tự động — không cần làm gì thêm.

## 2. Photo upload

### Field cần dùng
| Field | Ý nghĩa |
| --- | --- |
| `layer.id` | Định danh. |
| `layer.form_label` | Label. |
| `layer.show_value` | URL ảnh hiện tại (sau khi upload), nếu có. |
| `layer.value` | Giống `show_value` sau upload. |
| `layer.required` | Bắt buộc hay không. |

### Flow
1. User chạm "Upload photo" → mở file picker (native).
2. App nhận file → upload lên server/storage riêng → nhận URL public.
3. Gọi `service.changeUploadValue(layer, imageUrl, { file })`.
4. Refresh UI từ snapshot trả về.

**Package KHÔNG upload file hộ app.** App tự chọn nơi lưu (S3, GCS, Cloudinary, server riêng…), chỉ cần URL trả về là URL có thể GET được từ thiết bị.

### UI mẫu

```tsx
async function onPickPhoto(layer) {
  const file = await pickImage();           // expo-image-picker hoặc tương tự
  setUploading(true);
  try {
    const imageUrl = await uploadToBackend(file);
    applyChange(service.changeUploadValue(layer, imageUrl, { file }));
  } finally {
    setUploading(false);
  }
}

<View>
  <Text>{layer.form_label}{layer.required && ' *'}</Text>
  {layer.show_value
    ? <Image source={{ uri: layer.show_value }} />
    : <PlaceholderUploadButton onPress={() => onPickPhoto(layer)} />
  }
  <Button title={layer.show_value ? 'Change photo' : 'Upload photo'} onPress={() => onPickPhoto(layer)} />
</View>
```

### Lưu ý

- URL truyền vào phải là `https://…` hoặc `blob:…` — service không CDN-rewrite các URL đã có scheme.
- `metadata.file` (param thứ 3) là optional, dùng nếu app cần lưu lại file gốc cho bước remove-bg sau này. Có thể truyền `undefined` nếu không cần.
- Nếu campaign config có `layer.effects` (ví dụ `cutout.pro:face_cutout`), package không xử lý — app tự gọi service xử lý ảnh, nhận URL mới, rồi gọi lại `changeUploadValue`.

## 3. Option (single choice)

### Field cần dùng
| Field | Ý nghĩa |
| --- | --- |
| `layer.id` | Định danh. |
| `layer.form_label` | Label. |
| `layer.option_items` | Mảng item user có thể chọn. Mỗi item có `id`, `name`, `url`, `thumbnail`, `active`. |
| `layer.required` | Bắt buộc hay không. |

### UI mẫu

```tsx
<View>
  <Text>{layer.form_label}{layer.required && ' *'}</Text>
  <View style={{ flexDirection: 'row', flexWrap: 'wrap', gap: 8 }}>
    {layer.option_items.map((item) => (
      <Pressable
        key={item.id}
        onPress={() => applyChange(service.changeLayerOptionValue(item, layer))}
        style={[styles.optionTile, item.active && styles.optionTileActive]}
      >
        {item.url
          ? <Image source={{ uri: item.thumbnail || item.url }} style={{ width: 48, height: 48 }} />
          : <Text>{item.name}</Text>
        }
      </Pressable>
    ))}
  </View>
</View>
```

### Lưu ý

- `item.active` đã được service set sẵn cho item đang được chọn. App chỉ cần style theo cờ này.
- Sau khi gọi `changeLayerOptionValue`, snapshot mới sẽ có `option_items` với `active` đã cập nhật cho item vừa chọn → re-render là đủ.

## 4. Clipart

Render giống `option` nhưng tất cả `option_items` luôn là ảnh.

```tsx
<View>
  <Text>{layer.form_label}{layer.required && ' *'}</Text>
  <FlatList
    data={layer.option_items}
    numColumns={4}
    keyExtractor={(item) => String(item.id)}
    renderItem={({ item }) => (
      <Pressable
        onPress={() => applyChange(service.changeLayerOptionValue(item, layer))}
        style={[styles.clipartTile, item.active && styles.clipartTileActive]}
      >
        <Image source={{ uri: item.thumbnail || item.url }} style={{ width: 64, height: 64 }} />
      </Pressable>
    )}
  />
</View>
```

## 5. Grouped clipart

Đây là loại phức tạp nhất. `option_items` là mảng **group**, mỗi group có `options` bên trong là các clipart con.

### Cấu trúc dữ liệu

```ts
layer.option_items = [
  {
    id: 'group-1',
    name: 'Cats',
    thumbnail: 'https://…/cats.png',
    show_value: 'https://…/cats/black-cat.png',  // URL của item đang được chọn
    active: true,                                 // nhóm đang được chọn
    options: [
      { id: 'cat-1', name: 'Black cat', url: 'https://…/black-cat.png', active: true },
      { id: 'cat-2', name: 'White cat', url: 'https://…/white-cat.png', active: false },
      ...
    ],
  },
  {
    id: 'group-2',
    name: 'Dogs',
    options: [ ... ],
    ...
  },
];
```

### UI 2 cấp (khuyến nghị)

```tsx
function GroupedClipartPicker({ layer }) {
  const activeGroup = layer.option_items.find(g => g.active) || layer.option_items[0];

  return (
    <View>
      <Text>{layer.form_label}{layer.required && ' *'}</Text>

      {/* Cấp 1: chọn group */}
      <ScrollView horizontal>
        {layer.option_items.map((group) => (
          <Pressable
            key={group.id}
            onPress={() => applyChange(service.changeOptionGroupClipartValue(group, layer))}
            style={[styles.groupTab, group.active && styles.groupTabActive]}
          >
            {group.thumbnail
              ? <Image source={{ uri: group.thumbnail }} style={{ width: 40, height: 40 }} />
              : <Text>{group.name}</Text>
            }
          </Pressable>
        ))}
      </ScrollView>

      {/* Cấp 2: chọn item trong group */}
      <FlatList
        data={activeGroup.options}
        numColumns={4}
        keyExtractor={(item) => String(item.id)}
        renderItem={({ item }) => (
          <Pressable
            onPress={() => applyChange(service.changeLayerOptionValue(item, activeGroup, layer))}
            style={[styles.clipartTile, item.active && styles.clipartTileActive]}
          >
            <Image source={{ uri: item.thumbnail || item.url }} style={{ width: 64, height: 64 }} />
          </Pressable>
        )}
      />
    </View>
  );
}
```

### Hai cách gọi update

| Tình huống | Method | Tham số |
| --- | --- | --- |
| User chỉ chọn group, dùng item mặc định | `changeOptionGroupClipartValue(group, layer)` | `(group, layer)` |
| User chọn item cụ thể bên trong group | `changeLayerOptionValue(childItem, group, layer)` | `(childItem, group, layer)` |

Cả hai đều set đầy đủ `layer.value` (để qua validate) và `layer.show_value` (để render preview).

### Lưu ý

- Service tự đánh dấu `group.active` cho group chứa item đang chọn, và `item.active` cho item đó. Không cần app tự tính.
- Sau khi đổi group, có thể dùng `group.show_value` để biết item nào trong group đó đang active.
- Layer có thể có `clipart_reposition` hoặc `grouped_reposition` — service tự tính lại `top`/`left`/`width`/`height` cho design layer. App không cần quan tâm.

## 6. Visibility toggle (`form_visibility`)

Một số layer có `form_visibility == true`. Đây là switch user bật/tắt để hiển thị/ẩn layer trên preview (không phải dependency, mà là toggle thủ công).

### Phát hiện

```ts
const hasVisibilityToggle = !!layer.form_visibility;
const isCurrentlyVisible = layer.form_visibility_value !== false;
```

### UI mẫu

```tsx
{layer.form_visibility && (
  <View style={{ flexDirection: 'row', alignItems: 'center' }}>
    <Text>{layer.form_label}</Text>
    <Switch
      value={layer.form_visibility_value !== false}
      onValueChange={(checked) => {
        applyChange(service.changeLayerVisibility(layer, checked));
      }}
    />
  </View>
)}
```

Có thể đặt switch này NGANG HÀNG với label của layer, để user toggle ngay tại header section của option. Khi tắt, app vẫn nên render control bên dưới (mờ đi) hoặc ẩn hẳn — tùy UX.

### Lưu ý validation

- Layer text **luôn** được validate (kể cả khi `form_visibility_value = false`).
- Layer còn lại sẽ bị skip validate khi tắt visibility.

## 7. Dependency giữa các option

Một layer có thể chỉ hiện khi layer khác có giá trị nhất định (ví dụ: "tên thú cưng" chỉ hiện khi chọn "có thú cưng = yes"). Logic này đã được service xử lý **trước khi đưa vào `layerOptions`** — nếu một layer không thỏa dependency, nó sẽ không xuất hiện trong `layerOptions`.

→ App KHÔNG cần tự check dependency. Cứ render toàn bộ `layerOptions`. Sau mỗi `change*`, danh sách `layerOptions` tự co/giãn theo dependency mới.

Hiệu ứng UX cần lưu ý: số lượng option có thể thay đổi đột ngột sau thao tác. Nên dùng animation `LayoutAnimation`/`Reanimated` để mượt.

## 8. Validation feedback

Lấy từ snapshot:

```ts
const { isValid, errors } = snapshot.validation;
```

`errors` là mảng object có 2 dạng:

```ts
{ type: 'template', artworkId, message }
{ type: 'layer',    layerId,   label, message }
```

### Hiển thị inline error

```tsx
function getErrorForLayer(layerId, errors) {
  return errors.find(e => e.type === 'layer' && e.layerId === layerId);
}

<TextInput ... />
{getErrorForLayer(layer.id, snapshot.validation.errors) && (
  <Text style={{ color: 'red' }}>
    {getErrorForLayer(layer.id, snapshot.validation.errors).message}
  </Text>
)}
```

### Chỉ check trước khi add to cart

Nếu UX cho phép, nên ẨN error message khi user chưa thử submit. Một cách đơn giản:

```tsx
const [showErrors, setShowErrors] = useState(false);

function onAddToCart() {
  if (!snapshot.validation.isValid) {
    setShowErrors(true);
    scrollToFirstError();
    return;
  }
  // proceed
}
```

## 9. Template options (chọn mẫu thiết kế)

Khác với `layerOptions` (personalize từng field), `templateOptions` là chọn TEMPLATE (kiểu thiết kế tổng thể).

```ts
snapshot.templateOptions = [
  {
    artworkId: 'aw-1',
    label: 'Choose your style',
    optionValues: [
      { id: 'tpl-1', name: 'Modern', url: '…', thumbnail: '…', active: true, artworkId: 'aw-1' },
      { id: 'tpl-2', name: 'Vintage', url: '…', thumbnail: '…', active: false, artworkId: 'aw-1' },
    ],
    values: [/* same as optionValues */],
  },
  ...
];
```

### Render và update

```tsx
{snapshot.templateOptions.map((group) => (
  <View key={group.artworkId}>
    <Text>{group.label}</Text>
    <FlatList
      data={group.optionValues}
      numColumns={3}
      keyExtractor={(t) => String(t.id)}
      renderItem={({ item: template }) => (
        <Pressable
          onPress={() => applyChange(service.selectTemplate(template.id))}
          style={[styles.templateTile, template.active && styles.templateTileActive]}
        >
          <Image source={{ uri: template.thumbnail || template.url }} />
          <Text>{template.name}</Text>
        </Pressable>
      )}
    />
  </View>
))}
```

### Lưu ý quan trọng

- Khi gọi `selectTemplate`, toàn bộ `templateLayers` được clone lại từ template mới. Mọi giá trị user đã nhập ở các layer trùng `id` sẽ bị reset về default → nếu UX cần giữ lại giá trị (ví dụ tên user) qua các template, app phải tự cache và re-apply sau khi đổi template.
- Nếu campaign chỉ có 1 artwork và artwork chỉ có 1 template, service tự chọn sẵn — `templateOptions` vẫn có 1 group nhưng không cần render UI chọn.
- Nếu có nhiều artwork và user chưa chọn template, `validation.errors` sẽ có entry `type: 'template'`. App nên render template picker trước layer options trong trường hợp này.

## Tóm tắt flow xử lý option

```ts
// 1. Khởi tạo
const service = WM.initCustomizationTeeinblue(campaignData, options);
let snapshot = service.getSnapshot();
render(snapshot);

// 2. Mỗi tương tác user → gọi đúng method → apply snapshot mới
function onTextChange(layer, value) {
  snapshot = service.changeInputValue(layer, value);
  render(snapshot);
}

function onPickOption(item, layer) {
  snapshot = service.changeLayerOptionValue(item, layer);
  render(snapshot);
}

function onPickGroupItem(childItem, group, groupedLayer) {
  snapshot = service.changeLayerOptionValue(childItem, group, groupedLayer);
  render(snapshot);
}

function onPickGroupOnly(group, groupedLayer) {
  snapshot = service.changeOptionGroupClipartValue(group, groupedLayer);
  render(snapshot);
}

function onUploadDone(layer, imageUrl, file) {
  snapshot = service.changeUploadValue(layer, imageUrl, { file });
  render(snapshot);
}

function onToggleVisibility(layer, checked) {
  snapshot = service.changeLayerVisibility(layer, checked);
  render(snapshot);
}

function onPickTemplate(template) {
  snapshot = service.selectTemplate(template.id);
  render(snapshot);
}

// 3. Trước add to cart
function onSubmit() {
  if (!snapshot.validation.isValid) {
    showErrors(snapshot.validation.errors);
    return;
  }
  sendCartPayload({
    customization: snapshot.config,
    customizationState: service.getState(),  // để restore sau này
  });
}
```

## Checklist khi tích hợp

- [ ] Render `snapshot.layerOptions` theo `input_type` (5 loại đã liệt kê).
- [ ] Mỗi widget gọi đúng `service.change*` và **luôn** dùng snapshot trả về để re-render.
- [ ] Không tự lọc layer theo `visible`/`dependency` — đã được service làm sẵn.
- [ ] Render `form_visibility` switch khi `layer.form_visibility == true`.
- [ ] Render `templateOptions` khi user cần chọn mẫu.
- [ ] Show error từ `snapshot.validation.errors` khi user bấm add to cart.
- [ ] Gọi `service.getState()` để persist, truyền lại qua `options.state` khi mở lại màn hình.
- [ ] Gửi `snapshot.config` (lấy ngay trước submit) vào payload add to cart.
- [ ] Upload photo: app tự lo, chỉ truyền URL public vào `changeUploadValue`.
