# Customization TeeInBlue

Tài liệu này dành cho lập trình viên mobile cần tích hợp module `customization-teeinblue` từ package `@megaads/wm`.

Module nhận campaign data của TeeInBlue, chuẩn hóa dữ liệu thành các nhóm dễ dùng cho app mobile:

- Danh sách template để user chọn mẫu thiết kế.
- Danh sách option/layer để user nhập text, chọn option, chọn clipart hoặc upload ảnh.
- Dữ liệu preview gồm mockup, print area và design layer.
- Dữ liệu `config` để gửi khi add to cart.
- State để lưu và restore màn hình customization.

## Cài đặt

```sh
npm install @megaads/wm
```

## Khởi tạo service

```ts
import WM from "@megaads/wm";

const service = WM.initCustomizationTeeinblue(campaignData, {
  campaignProductIndex: 0,
  campaignMockupId: undefined,
  mockupId: undefined,
  printareaId: undefined,
  templateId: savedState?.templateId,
  state: savedState,
});

const snapshot = service.getSnapshot();
```

`campaignData` có thể truyền toàn bộ response hoặc phần `result`. Constructor tự xử lý cả hai dạng:

```ts
WM.initCustomizationTeeinblue(apiResponse);
WM.initCustomizationTeeinblue(apiResponse.result);
```

## Init options

| Field | Type | Ý nghĩa |
| --- | --- | --- |
| `campaignProductIndex` | `number` | Index của product trong `campaign_products`. Mặc định là `0`. |
| `campaignMockupId` | `string \| number` | Chọn campaign mockup theo `campaign_mockup.id`. |
| `mockupId` | `string \| number` | Chọn mockup theo `campaign_mockup.mockup_id`. Nên truyền cùng `printareaId` khi dùng variant mockup. |
| `printareaId` | `string \| number` | Chọn artwork theo print area tương ứng. |
| `templateId` | `string \| number` | Template được active ban đầu. |
| `state` | `object` | State đã lưu từ `service.getState()` để restore lựa chọn trước đó. |

Nếu không truyền mockup, service tự chọn theo thứ tự: variant có `campaign_mockup_id`, variant có `mockup_id + printarea_id`, mockup duy nhất, mockup có `mockup_id` hoặc `old_campaign_mockup_id`, mockup có print area, rồi fallback về item đầu tiên.

Về template: nếu campaign chỉ có 1 artwork, service tự chọn template đầu tiên. Nếu có nhiều artwork và app không truyền `templateId`, service **không auto-select** — user phải tự chọn template, và `validation.errors` sẽ có entry `type: 'template'` cho đến khi user chọn.

> Để biết cách render `layerOptions` và `templateOptions` cùng các method update, xem [customization-teeinblue-options.md](./customization-teeinblue-options.md).

## Luồng tích hợp khuyến nghị

1. Fetch campaign data từ API backend/app.
2. Khởi tạo service bằng `WM.initCustomizationTeeinblue(campaignData, options)`.
3. Gọi `getSnapshot()` để render màn hình lần đầu.
4. Khi user thao tác, gọi method thay đổi tương ứng.
5. Sau mỗi thao tác, lấy snapshot mới từ return value hoặc `getSnapshot()` rồi cập nhật UI.
6. Trước khi add to cart, kiểm tra `snapshot.validation.isValid`.
7. Gửi `snapshot.config` trong payload add to cart.
8. Lưu `service.getState()` nếu cần restore khi quay lại màn hình.

Ví dụ tổng quát:

```ts
let snapshot = service.getSnapshot();

function refresh(nextSnapshot?: any) {
  snapshot = nextSnapshot ?? service.getSnapshot();
  renderCustomization(snapshot);
}

function onSelectTemplate(templateId: string | number) {
  refresh(service.selectTemplate(templateId));
}

function onSelectOption(layer: any, optionItem: any) {
  refresh(service.changeLayerOptionValue(optionItem, layer));
}

function onChangeText(layer: any, text: string) {
  refresh(service.changeInputValue(layer, text));
}

function onUploadPhoto(layer: any, imageUrl: string, file?: unknown) {
  refresh(service.changeUploadValue(layer, imageUrl, { file }));
}

function onToggleLayer(layer: any, checked: boolean) {
  refresh(service.changeLayerVisibility(layer, checked));
}

function buildCartPayload() {
  const latest = service.getSnapshot();

  if (!latest.validation.isValid) {
    return {
      ok: false,
      errors: latest.validation.errors,
    };
  }

  return {
    ok: true,
    customization: latest.config,
    customizationState: service.getState(),
  };
}
```

## Snapshot

`getSnapshot()` là API chính để mobile render toàn bộ màn hình.

```ts
const snapshot = service.getSnapshot();
```

Snapshot trả về:

| Field | Ý nghĩa |
| --- | --- |
| `campaign` | Campaign data đã clone và normalize. |
| `campaignProduct` | Product đang active trong campaign. |
| `campaignMockup` | Campaign mockup đang active. |
| `mockup` | Dữ liệu preview mockup đã rút gọn cho renderer. |
| `printAreas` | Danh sách vùng in kèm design layers. |
| `artwork` | Artwork đang active. |
| `template` | Template đang active, có `layers` đã được apply value hiện tại. |
| `templateOptions` | Danh sách nhóm template cho UI chọn template. |
| `layerOptions` | Danh sách field personalization để render form. |
| `designLayers` | Danh sách layer text/image để render trong print area. |
| `fonts` | Font cần load cho text layer. |
| `config` | Payload customization để gửi add to cart. |
| `validation` | Kết quả validate required fields. |

## Render template options

`snapshot.templateOptions` là danh sách nhóm template theo artwork.

```ts
type TemplateOptionGroup = {
  artworkId: string | number;
  label: string;
  optionValues: TemplateOptionValue[];
  values: TemplateOptionValue[];
};

type TemplateOptionValue = {
  id: string | number;
  name: string;
  title: string;
  url: string | null;
  thumbnail: string | null;
  active: boolean;
  artworkId: string | number;
};
```

Render mỗi group như một selector. Khi user chọn template:

```ts
const nextSnapshot = service.selectTemplate(template.id);
```

Lưu ý: chọn template sẽ thay toàn bộ `templateLayers`, vì vậy UI nên refresh từ snapshot mới thay vì giữ reference layer cũ.

## Render layer options

`snapshot.layerOptions` là danh sách control mà user có thể thao tác. Các layer đã được lọc theo `visible`, dependency, `form_type`, `linked`, `group`.

Field quan trọng:

| Field | Ý nghĩa |
| --- | --- |
| `id` | ID layer, dùng lại khi gọi method update. |
| `form_label` | Label hiển thị cho user. |
| `input_type` | Loại control đã normalize. |
| `option_items` | Danh sách item cho option/clipart/grouped clipart. |
| `options` | Alias của `option_items`. |
| `value` | Giá trị logic hiện tại. |
| `show_value` | Giá trị hiển thị/render hiện tại. |
| `active` | Với option item, đánh dấu item đang chọn. |
| `required` | Field bắt buộc nếu `true`. |
| `max_length` | Giới hạn text nếu có. |
| `form_visibility_value` | Trạng thái bật/tắt với layer có visibility control. |

Các `input_type` phổ biến:

| `input_type` | UI mobile nên render |
| --- | --- |
| `text` | Text input. Dùng `max_length` nếu có. |
| `photo` | Upload/select photo. Sau khi upload xong truyền URL vào `changeUploadValue`. |
| `option` | Single choice list/grid. |
| `clipart` | Grid ảnh, chọn trực tiếp `option_items`. |
| `grouped_clipart` | Chọn group trước, sau đó chọn item bên trong `group.options`. |

### Text

```ts
const textLayer = snapshot.layerOptions.find((layer: any) => layer.input_type === "text");
const nextSnapshot = service.changeInputValue(textLayer, "Alex");
```

Với layer có `form_input_textcase: "uppercase"`, service sẽ tự uppercase khi tạo `designLayers`.

### Photo upload

```ts
const photoLayer = snapshot.layerOptions.find((layer: any) => layer.input_type === "photo");

// imageUrl là URL đã upload lên server/storage của app.
const nextSnapshot = service.changeUploadValue(photoLayer, imageUrl, {
  file: localFileMetadata,
});
```

Service không upload file hộ mobile app. Mobile cần upload ảnh trước, nhận URL public hoặc URL mà renderer đọc được, rồi truyền URL đó vào `changeUploadValue`.

### Option và clipart

```ts
const optionLayer = snapshot.layerOptions.find((layer: any) => layer.input_type === "option");
const item = optionLayer.option_items[0];

const nextSnapshot = service.changeLayerOptionValue(item, optionLayer);
```

Với `clipart`, cách gọi tương tự:

```ts
const clipartLayer = snapshot.layerOptions.find((layer: any) => layer.input_type === "clipart");
const item = clipartLayer.option_items[0];

service.changeLayerOptionValue(item, clipartLayer);
```

### Grouped clipart

`grouped_clipart` có cấu trúc 2 cấp:

```ts
const groupedLayer = snapshot.layerOptions.find(
  (layer: any) => layer.input_type === "grouped_clipart",
);

const group = groupedLayer.option_items[0];
const childItem = group.options[0];

service.changeLayerOptionValue(childItem, group, groupedLayer);
```

Nếu UI chỉ cần chọn group mặc định, có thể dùng:

```ts
service.changeOptionGroupClipartValue(group, groupedLayer);
```

## Render preview

Mobile có 2 hướng render preview:

- Dùng `snapshot.mockup` để render mockup hoàn chỉnh gồm background/mockup image và print area.
- Dùng `snapshot.printAreas` nếu renderer của app tách vùng in riêng.

### Mockup preview

```ts
type MockupPreview = {
  id: string | number;
  width: number;
  height: number;
  preview_url: string | null;
  preview_thumbnail: string | null;
  layers: MockupPreviewLayer[];
};
```

`mockup.layers` gồm:

| `type` | Ý nghĩa |
| --- | --- |
| `image` | Layer ảnh của mockup. Render bằng `src`/`url`. |
| `printarea` | Vùng đặt design. Render `layers` bên trong theo kích thước vùng này. |

Các field tọa độ chính: `top`, `left`, `width`, `height`, `rotate`, `opacity`.

### Design layers

`snapshot.designLayers` và `printarea.layers` chứa layer thật cần vẽ trong vùng in.

Layer ảnh:

```ts
type ImageDesignLayer = {
  id: string | number;
  type: "image";
  src: string;
  url: string;
  top: number;
  left: number;
  width: number;
  height: number;
  rotate: number;
  opacity: number;
  masked_enable?: boolean;
  masked_image?: string | null;
};
```

Layer text:

```ts
type TextDesignLayer = {
  id: string | number;
  type: "text";
  text: string;
  defaultText?: string;
  top: number;
  left: number;
  width: number;
  height: number;
  rotate: number;
  color?: string;
  align?: string;
  typography?: {
    family?: string;
    variant?: string;
    size?: number;
  };
  typography_type?: "google" | "custom";
  custom_font?: {
    family?: string;
    url?: string;
  } | null;
  stroke_enabled?: boolean;
  stroke_color?: string;
  stroke_width?: number;
  autoscale_enabled?: boolean;
  autoscale_max_width?: number;
};
```

Renderer nên sort layer theo `order` tăng dần nếu cần kiểm soát z-index.

## Load font

`snapshot.fonts` cho biết font nào cần load trước khi render text:

```ts
{
  google: {
    "Roboto": ["regular", "700"]
  },
  custom: {
    "My Font": {
      family: "My Font",
      url: "https://cdn.teeinblue.com/path/font.woff2"
    }
  }
}
```

Với custom font, service đổi đuôi `.otf`/`.ttf` sang `.woff2` nếu layer có `custom_font.url`.

## Validate trước add to cart

```ts
const { validation } = service.getSnapshot();

if (!validation.isValid) {
  showErrors(validation.errors);
  return;
}
```

Error format:

```ts
type ValidationError =
  | {
      type: "template";
      artworkId: string | number;
      message: string;
    }
  | {
      type: "layer";
      layerId: string | number;
      label: string;
      message: string;
    };
```

Service validate:

- Template required khi artwork có `template_settings` và có nhiều hơn 1 template.
- Layer required khi layer đang visible, đang thỏa dependency, không phải static/linked, có `form_label`, và chưa có `value` hoặc `upload_image_url`.

## Payload add to cart

Dùng `snapshot.config` hoặc `service.getCustomizationConfig()`.

```ts
const snapshot = service.getSnapshot();

const payload = {
  product_id: productId,
  variant_id: variantId,
  quantity,
  customization: snapshot.config,
};
```

`config` có dạng:

```ts
{
  disable_make_change: true,
  custom_type: {
    name: "teeinblue",
  },
  options: [
    {
      name: "Pet",
      value: "https://cdn.teeinblue.com/...",
      optionItem: {
        id: "option-id",
        name: "Dog",
        url: "https://cdn.teeinblue.com/..."
      }
    }
  ],
  layers: [
    {
      name: "Name",
      type: "text",
      value: "Alex",
      // Các transform config còn lại của layer được giữ lại.
    }
  ]
}
```

`options` dùng để hiển thị tóm tắt lựa chọn của user. `layers` là dữ liệu chi tiết để backend/order system tái dựng personalization.

## Lưu và restore state

Khi user rời màn hình hoặc cần cache customization:

```ts
const state = service.getState();
```

Restore:

```ts
const service = WM.initCustomizationTeeinblue(campaignData, {
  templateId: state.templateId,
  state,
});
```

State gồm:

```ts
{
  templateId: "template-391321",
  layers: {
    "layer-1": {
      value: "Alex",
      show_value: "Alex",
      form_visibility_value: true,
      selected_name: "Dog",
      optionItem: {}
    }
  }
}
```

Chỉ nên persist object từ `getState()`. Không nên tự build state thủ công nếu không cần thiết.

## Public methods

| Method | Mục đích |
| --- | --- |
| `getSnapshot()` | Lấy toàn bộ data để render UI, preview, config, validation. |
| `getData()` | Alias của `getSnapshot()`. |
| `selectTemplate(templateId)` | Chọn template, rebuild layer/options/preview. |
| `changeLayerOptionValue(optionItem, option, parentLayer?)` | Chọn option, clipart hoặc child item của grouped clipart. |
| `changeOptionValue(optionItem, option, parentLayer?)` | Method gốc cho chọn option. |
| `changeOptionGroupClipartValue(optionItem, option)` | Chọn grouped clipart theo group. |
| `changeInputValue(layerOption, value)` | Update text layer và các linked layer. |
| `changeUploadValue(layerOption, imageUrl, metadata?)` | Update photo/upload layer và các linked layer. |
| `changeLayerVisibility(layerOption, value)` | Bật/tắt layer có `form_visibility`. |
| `validate()` | Validate required template/layer. |
| `getCustomizationConfig()` | Lấy payload add to cart. |
| `getState()` | Lấy state để persist. |
| `getFonts()` | Lấy danh sách Google/custom font cần load. |
| `getPreview()` | Alias của `getMockupPreview()`. |
| `getMockupLayers()` | Lấy raw mockup layers đang active. |
| `getDesignLayers()` | Lấy design layers đang active. |
| `getLayerOptions()` | Lấy personalization controls. |
| `getTemplateOptions()` | Lấy template controls. |
| `getPrintAreas()` | Lấy print areas. |
| `getTemplate()` | Lấy template active kèm layers đã update. |
| `getTemplateLayers()` | Lấy raw template layers đang active. |
| `setTemplateLayers(layers)` | Set lại template layers rồi rebuild. Chỉ dùng cho case nâng cao/debug. |
| `setTemplateOptions(options)` | Override template options. Chỉ dùng cho case nâng cao/debug. |

## Lưu ý tích hợp mobile

- Sau mọi thao tác thay đổi, dùng snapshot mới để render lại vì service có thể rebuild dependency, active state, design layers và config.
- Không giữ reference layer/option quá lâu sau khi gọi `selectTemplate`, vì template layer đã được clone lại.
- URL asset tương đối được chuẩn hóa sang `https://cdn.teeinblue.com/...`. URL `http`, `https`, và `blob` được giữ nguyên.
- Với upload ảnh, app chịu trách nhiệm upload và truyền URL sau upload. Package không xử lý upload file trong public flow này.
- Dependency giữa các layer được xử lý trong service. UI chỉ cần render `snapshot.layerOptions` hiện tại.
- `snapshot.config` nên được lấy lại ngay trước add to cart để tránh gửi payload cũ.
- Nếu render preview bằng canvas/native view, cần quy đổi tọa độ theo ratio giữa kích thước thiết kế (`mockup.width/height` hoặc `printarea.width/height`) và kích thước hiển thị thực tế.
