Referência Histórica

ESPECIFICAÇÃO - PERFIL DO FORNECEDOR (SUPPLIER)

Data: 2026-01-14 Hora: 13:38 Status: ✅ Aprovado para Implementação Epic: AGR-152 (Validação de Perfil)

ESPECIFICAÇÃO - PERFIL DO FORNECEDOR (SUPPLIER)

Data: 2026-01-14 Hora: 13:38 Status: ✅ Aprovado para Implementação Epic: AGR-152 (Validação de Perfil)


📋 VISÃO GERAL

Implementação completa da página /profile do fornecedor, incluindo validação de e-mail, gestão de endereços, seleção de tipos de produtos e configuração de área de atuação geográfica.


🎯 OBJETIVOS

Objetivo Principal

Permitir que fornecedores completem seu cadastro de forma simples e intuitiva, garantindo informações mínimas necessárias para participar de licitações.

Critérios de Sucesso

  • ✅ 80%+ dos fornecedores completam o perfil
  • ✅ Tempo médio < 5 minutos para completar
  • ✅ Taxa de abandono < 15% na seção geográfica
  • ✅ Taxa de erros de validação < 5%

📐 ARQUITETURA DA SOLUÇÃO

Estrutura de Componentes

apps/supplier/pages/profile.vue
├── Header com Avatar e Informações Básicas
├── Barra de Progresso (AGR-152)
├── Card 1: Validação de E-mail (reutilizar producer)
├── Card 2: Endereços (reutilizar producer)
├── Card 3: Tipos de Produtos (NOVO)
│   └── components/profile/ProductTypesSelector.vue
└── Card 4: Área de Atuação Geográfica (NOVO)
    ├── components/profile/GeographicAreaSelector.vue
    ├── components/profile/GeographicStatesSelector.vue
    └── components/profile/GeographicCitiesAutocomplete.vue

🗄️ ESTRUTURA DE BANCO DE DADOS

Tabela Nova: supplier_product_types (Pivot)

CREATE TABLE supplier_product_types (
    id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    supplier_id BIGINT UNSIGNED NOT NULL,
    product_type_id BIGINT UNSIGNED NOT NULL,
    status BOOLEAN DEFAULT TRUE,
    created_at TIMESTAMP NULL,
    updated_at TIMESTAMP NULL,

    UNIQUE KEY unique_supplier_product (supplier_id, product_type_id),
    INDEX idx_supplier_id (supplier_id),
    INDEX idx_product_type_id (product_type_id),

    FOREIGN KEY (supplier_id) REFERENCES suppliers(id) ON DELETE CASCADE,
    FOREIGN KEY (product_type_id) REFERENCES product_types(id) ON DELETE CASCADE
);

Tabelas Existentes (Já Criadas)

-- Tipos de produtos (5 tipos cadastrados)
product_types
├── id
├── description (varchar)
├── status (boolean)
└── timestamps

-- Estados atendidos pelo fornecedor
supplier_states
├── id
├── supplier_id
├── state_id
├── status
└── timestamps

-- Cidades atendidas pelo fornecedor
supplier_state_cities
├── id
├── supplier_id
├── state_city_id
├── status
└── timestamps

-- Estados brasileiros (27 estados)
states
├── id
├── sigla (varchar 2)
├── nome (varchar)
└── timestamps

-- Cidades (5.570 cidades)
state_cities
├── id
├── state_id
├── nome (varchar)
└── timestamps

🔌 ESPECIFICAÇÃO DE APIs

1. Tipos de Produtos

GET /api/v1/product-types

Lista todos os tipos de produtos disponíveis (5 tipos).

Response:

{
  "data": [
    {
      "id": 1,
      "description": "Defensivos Agrícolas",
      "status": true
    },
    {
      "id": 2,
      "description": "Fertilizantes & Nutrição",
      "status": true
    },
    {
      "id": 3,
      "description": "Sementes",
      "status": true
    },
    {
      "id": 4,
      "description": "Adjuvantes & Modificadores de Calda",
      "status": true
    },
    {
      "id": 5,
      "description": "Bioinsumos",
      "status": true
    }
  ]
}

GET /api/v1/supplier/product-types

Retorna os tipos de produtos que o fornecedor selecionou.

Response:

{
  "data": [
    {
      "id": 1,
      "description": "Defensivos Agrícolas",
      "selected_at": "2026-01-14T13:30:00Z"
    },
    {
      "id": 3,
      "description": "Sementes",
      "selected_at": "2026-01-14T13:30:00Z"
    }
  ]
}

POST /api/v1/supplier/product-types

Salva os tipos de produtos selecionados pelo fornecedor.

Request:

{
  "product_type_ids": [1, 3, 5]
}

Response:

{
  "message": "Tipos de produtos atualizados com sucesso",
  "data": {
    "product_types": [
      { "id": 1, "description": "Defensivos Agrícolas" },
      { "id": 3, "description": "Sementes" },
      { "id": 5, "description": "Bioinsumos" }
    ]
  }
}

Validação:

  • Mínimo 1 tipo de produto deve ser selecionado
  • IDs devem existir na tabela product_types

2. Geografia (Estados e Cidades)

GET /api/v1/geography/states

Lista todos os estados brasileiros (27 estados).

Response:

{
  "data": [
    { "id": 1, "sigla": "AC", "nome": "Acre" },
    { "id": 2, "sigla": "AL", "nome": "Alagoas" },
    { "id": 25, "sigla": "SP", "nome": "São Paulo" },
    // ... 27 estados
  ]
}

GET /api/v1/geography/cities?state_id=25

Lista cidades de um estado específico.

Query Parameters:

  • state_id (required): ID do estado

Response:

{
  "data": [
    { "id": 1234, "nome": "São Paulo", "state_id": 25 },
    { "id": 1235, "nome": "Campinas", "state_id": 25 },
    { "id": 1236, "nome": "Santos", "state_id": 25 },
    // ... todas cidades do estado
  ],
  "meta": {
    "total": 645,
    "state": { "id": 25, "sigla": "SP", "nome": "São Paulo" }
  }
}

GET /api/v1/geography/cities/search?q=Uber

Busca cidades por nome (autocomplete).

Query Parameters:

  • q (required): Termo de busca (mínimo 3 caracteres)
  • limit (optional): Limite de resultados (default: 10)

Response:

{
  "data": [
    {
      "id": 1234,
      "nome": "Uberlândia",
      "state_id": 31,
      "state": { "sigla": "MG", "nome": "Minas Gerais" }
    },
    {
      "id": 1235,
      "nome": "Uberaba",
      "state_id": 31,
      "state": { "sigla": "MG", "nome": "Minas Gerais" }
    },
    {
      "id": 1236,
      "nome": "Ubatuba",
      "state_id": 25,
      "state": { "sigla": "SP", "nome": "São Paulo" }
    }
  ],
  "meta": {
    "total": 3,
    "query": "Uber"
  }
}

Otimizações:

  • Cache de 1 hora para lista de estados (são fixos)
  • Debounce de 300ms no frontend para autocomplete
  • Limite padrão de 10 resultados para performance

3. Área de Atuação Geográfica

GET /api/v1/supplier/geographic-area

Retorna a área de atuação configurada pelo fornecedor.

Response:

{
  "data": {
    "scope": "states",
    "states": [
      { "id": 25, "sigla": "SP", "nome": "São Paulo" },
      { "id": 31, "sigla": "MG", "nome": "Minas Gerais" },
      { "id": 33, "sigla": "RJ", "nome": "Rio de Janeiro" }
    ],
    "cities": [
      {
        "id": 2345,
        "nome": "Goiânia",
        "state": { "id": 52, "sigla": "GO", "nome": "Goiás" }
      },
      {
        "id": 3456,
        "nome": "Brasília",
        "state": { "id": 53, "sigla": "DF", "nome": "Distrito Federal" }
      }
    ],
    "summary": {
      "is_national": false,
      "states_count": 3,
      "cities_count": 2,
      "total_coverage": "3 estados + 2 cidades adicionais"
    }
  }
}

Caso Nacional:

{
  "data": {
    "scope": "national",
    "states": [],
    "cities": [],
    "summary": {
      "is_national": true,
      "states_count": 0,
      "cities_count": 0,
      "total_coverage": "Todo o Brasil"
    }
  }
}

POST /api/v1/supplier/geographic-area

Salva a área de atuação do fornecedor.

Request (Nacional):

{
  "scope": "national"
}

Request (Por Estados):

{
  "scope": "states",
  "state_ids": [25, 31, 33],
  "city_ids": [2345, 3456]
}

Request (Por Cidades):

{
  "scope": "cities",
  "city_ids": [1234, 1235, 1236, 1237, 1238]
}

Response:

{
  "message": "Área de atuação atualizada com sucesso",
  "data": {
    "scope": "states",
    "summary": {
      "is_national": false,
      "states_count": 3,
      "cities_count": 2,
      "total_coverage": "3 estados + 2 cidades adicionais"
    }
  }
}

Validação:

  • Se scope = "national": não precisa state_ids nem city_ids
  • Se scope = "states": state_ids obrigatório com mínimo 1 item
  • Se scope = "cities": city_ids obrigatório com mínimo 1 item
  • IDs devem existir nas tabelas correspondentes

Lógica de Salvamento:

  1. Limpar registros antigos de supplier_states e supplier_state_cities
  2. Se nacional: não inserir nenhum registro (flag é a ausência de registros + scope)
  3. Se estados: inserir em supplier_states
  4. Se cidades: inserir em supplier_state_cities
  5. Permitir combinação: estados + cidades adicionais

4. Validação de Perfil

GET /api/v1/supplier/profile/validation

Retorna o status de validação do perfil do fornecedor.

Response:

{
  "data": {
    "is_complete": true,
    "progress": 100,
    "validation": {
      "email_verified": {
        "status": true,
        "verified_at": "2026-01-14T10:30:00Z"
      },
      "has_address": {
        "status": true,
        "count": 2
      },
      "has_product_types": {
        "status": true,
        "count": 3,
        "types": ["Defensivos Agrícolas", "Sementes", "Bioinsumos"]
      },
      "has_geographic_area": {
        "status": true,
        "scope": "states",
        "summary": "3 estados + 2 cidades adicionais"
      }
    },
    "missing_items": [],
    "next_steps": []
  }
}

Response (Perfil Incompleto):

{
  "data": {
    "is_complete": false,
    "progress": 50,
    "validation": {
      "email_verified": {
        "status": true,
        "verified_at": "2026-01-14T10:30:00Z"
      },
      "has_address": {
        "status": true,
        "count": 1
      },
      "has_product_types": {
        "status": false,
        "count": 0,
        "types": []
      },
      "has_geographic_area": {
        "status": false,
        "scope": null,
        "summary": null
      }
    },
    "missing_items": [
      "Selecione pelo menos 1 tipo de produto",
      "Configure sua área de atuação geográfica"
    ],
    "next_steps": [
      "Acesse a aba 'Tipos de Produtos' e selecione os insumos que você fornece",
      "Configure em qual região você atende (Nacional, Estados ou Cidades)"
    ]
  }
}

Cálculo de Progresso:

// 4 itens obrigatórios (25% cada)
const progress =
  (emailVerified ? 25 : 0) +
  (hasAddress ? 25 : 0) +
  (hasProductTypes ? 25 : 0) +
  (hasGeographicArea ? 25 : 0)

🎨 SOLUÇÃO DE UX/UI

Princípios de Design Aplicados

1. Lei de Miller (Chunking Information)

  • Máximo 7±2 itens visíveis por vez
  • Selecionados separados visualmente
  • Lista completa com scroll

2. Lei de Hick (Reduzir Decisões)

  • Fluxo guiado: uma decisão por vez
  • 3 opções claras iniciais (Nacional/Estados/Cidades)
  • Progressive disclosure

3. Efeito do Gradiente de Objetivo

  • Barra de progresso sempre visível
  • Checklist visual (✅/⚠️)
  • Feedback imediato

4. Lei da Conectividade Uniforme

  • Cards separados por seção
  • Status visual claro (cor verde = completo, amarelo = pendente)
  • Agrupamento hierárquico

5. Lei de Prägnanz (Simplicidade)

  • Design minimalista
  • Uma ação por vez
  • Botões grandes e labels claras

Fluxo de Área de Atuação Geográfica

Passo 1: Escolha do Escopo

┌─────────────────────────────────────────────────────────────┐
│  📍 Onde você atende?                                        │
│                                                              │
│  [ ] 🇧🇷 NACIONAL - Atendo todo o Brasil                    │
│  [✓] 🗺️ POR ESTADOS - Escolho estados específicos          │
│  [ ] 📍 POR CIDADES - Escolho cidades específicas           │
│                                                              │
│  [Continuar]                                                 │
└─────────────────────────────────────────────────────────────┘

Passo 2A: Nacional (Fim)

┌─────────────────────────────────────────────────────────────┐
│  ✅ Área de Atuação: NACIONAL                               │
│                                                              │
│  🇧🇷 Você atende todo o Brasil                              │
│                                                              │
│  [Editar]                                                    │
└─────────────────────────────────────────────────────────────┘

Passo 2B: Por Estados

┌─────────────────────────────────────────────────────────────┐
│  🗺️ Selecione os estados onde você atende:                 │
│                                                              │
│  🔍 [Buscar estado...                          ] [Limpar]   │
│                                                              │
│  ✅ Selecionados (3):                                       │
│  ┌─────────────┬─────────────┬─────────────┐              │
│  │ ✅ SP       │ ✅ MG       │ ✅ RJ       │              │
│  │ São Paulo  │ Minas Gerais│ Rio Janeiro │              │
│  │    [×]     │    [×]      │    [×]      │              │
│  └─────────────┴─────────────┴─────────────┘              │
│                                                              │
│  📋 Todos os estados:                                       │
│  □ AC  □ AL  □ AM  □ AP  □ BA  □ CE  □ DF  □ ES           │
│  □ GO  □ MA  □ MT  □ MS  □ PA  □ PB  □ PE  □ PI           │
│  □ PR  □ RN  □ RO  □ RR  □ RS  □ SC  □ SE  □ TO           │
│                                                              │
│  💡 [+ Adicionar cidades de outros estados]                │
│                                                              │
│  [Salvar Seleção]                                           │
└─────────────────────────────────────────────────────────────┘

Passo 2C: Por Cidades (Autocomplete)

┌─────────────────────────────────────────────────────────────┐
│  📍 Digite o nome da cidade:                                │
│                                                              │
│  🔍 [Uber___________________________]                       │
│      │                                                       │
│      │ Sugestões:                                           │
│      │  • Uberlândia - MG                                  │
│      │  • Uberaba - MG                                     │
│      │  • Ubatuba - SP                                     │
│      └─────────────────────────────────────────────────────│
│                                                              │
│  ✅ Cidades Selecionadas (5):                               │
│  • Uberlândia - MG                              [×]         │
│  • Goiânia - GO                                 [×]         │
│  • São Paulo - SP                               [×]         │
│  • Campinas - SP                                [×]         │
│  • Brasília - DF                                [×]         │
│                                                              │
│  💡 Atendendo muitas cidades de um estado?                  │
│     [← Voltar e selecionar estados inteiros]                │
│                                                              │
│  [Salvar Seleção]                                           │
└─────────────────────────────────────────────────────────────┘

Passo 3: Resumo Visual

┌─────────────────────────────────────────────────────────────┐
│  ✅ Área de Atuação Configurada                             │
│                                                              │
│  🗺️ Você atende:                                            │
│                                                              │
│  📌 ESTADOS (3):                                            │
│  • São Paulo (SP) - todas cidades                           │
│  • Minas Gerais (MG) - todas cidades                        │
│  • Rio de Janeiro (RJ) - todas cidades                      │
│                                                              │
│  📍 CIDADES ADICIONAIS (2):                                 │
│  • Goiânia - GO                                             │
│  • Brasília - DF                                            │
│                                                              │
│  💡 Você receberá licitações destas regiões                 │
│                                                              │
│  [Editar Área de Atuação]                                   │
└─────────────────────────────────────────────────────────────┘

💻 ESTRUTURA DE CÓDIGO

Composables

composables/useSupplierProfile.ts

export interface ProductType {
  id: number
  description: string
  status: boolean
}

export interface GeographicArea {
  scope: 'national' | 'states' | 'cities'
  states: Array<{ id: number; sigla: string; nome: string }>
  cities: Array<{
    id: number
    nome: string
    state: { id: number; sigla: string; nome: string }
  }>
  summary: {
    is_national: boolean
    states_count: number
    cities_count: number
    total_coverage: string
  }
}

export interface ProfileValidation {
  is_complete: boolean
  progress: number
  validation: {
    email_verified: { status: boolean; verified_at?: string }
    has_address: { status: boolean; count: number }
    has_product_types: { status: boolean; count: number; types: string[] }
    has_geographic_area: { status: boolean; scope?: string; summary?: string }
  }
  missing_items: string[]
  next_steps: string[]
}

export const useSupplierProfile = () => {
  const { user } = useAuth()
  const config = useRuntimeConfig()

  // Estados
  const productTypes = ref<ProductType[]>([])
  const selectedProductTypes = ref<number[]>([])
  const geographicArea = ref<GeographicArea | null>(null)
  const profileValidation = ref<ProfileValidation | null>(null)
  const loading = ref(false)
  const error = ref<string | null>(null)

  // Computeds
  const isProfileComplete = computed(() =>
    profileValidation.value?.is_complete || false
  )

  const profileProgress = computed(() =>
    profileValidation.value?.progress || 0
  )

  // Métodos
  const loadProductTypes = async () => {
    try {
      loading.value = true
      const response = await $fetch<{ data: ProductType[] }>(
        '/api/v1/product-types',
        { baseURL: config.public.apiBase }
      )
      productTypes.value = response.data
    } catch (e) {
      error.value = 'Erro ao carregar tipos de produtos'
      console.error(e)
    } finally {
      loading.value = false
    }
  }

  const loadSelectedProductTypes = async () => {
    try {
      const response = await $fetch<{ data: ProductType[] }>(
        '/api/v1/supplier/product-types',
        { baseURL: config.public.apiBase }
      )
      selectedProductTypes.value = response.data.map(pt => pt.id)
    } catch (e) {
      console.error(e)
    }
  }

  const saveProductTypes = async (typeIds: number[]) => {
    try {
      loading.value = true
      await $fetch('/api/v1/supplier/product-types', {
        method: 'POST',
        baseURL: config.public.apiBase,
        body: { product_type_ids: typeIds }
      })
      selectedProductTypes.value = typeIds
      await validateProfile()
    } catch (e) {
      error.value = 'Erro ao salvar tipos de produtos'
      console.error(e)
      throw e
    } finally {
      loading.value = false
    }
  }

  const loadGeographicArea = async () => {
    try {
      const response = await $fetch<{ data: GeographicArea }>(
        '/api/v1/supplier/geographic-area',
        { baseURL: config.public.apiBase }
      )
      geographicArea.value = response.data
    } catch (e) {
      console.error(e)
    }
  }

  const saveGeographicArea = async (area: Partial<GeographicArea>) => {
    try {
      loading.value = true
      const payload: any = { scope: area.scope }

      if (area.scope === 'states' && area.states) {
        payload.state_ids = area.states.map(s => s.id)
      }

      if (area.cities && area.cities.length > 0) {
        payload.city_ids = area.cities.map(c => c.id)
      }

      const response = await $fetch<{ data: GeographicArea }>(
        '/api/v1/supplier/geographic-area',
        {
          method: 'POST',
          baseURL: config.public.apiBase,
          body: payload
        }
      )

      geographicArea.value = response.data
      await validateProfile()
    } catch (e) {
      error.value = 'Erro ao salvar área de atuação'
      console.error(e)
      throw e
    } finally {
      loading.value = false
    }
  }

  const validateProfile = async () => {
    try {
      const response = await $fetch<{ data: ProfileValidation }>(
        '/api/v1/supplier/profile/validation',
        { baseURL: config.public.apiBase }
      )
      profileValidation.value = response.data
    } catch (e) {
      console.error(e)
    }
  }

  // Inicialização
  onMounted(() => {
    loadProductTypes()
    loadSelectedProductTypes()
    loadGeographicArea()
    validateProfile()
  })

  return {
    productTypes,
    selectedProductTypes,
    geographicArea,
    profileValidation,
    isProfileComplete,
    profileProgress,
    loading,
    error,
    saveProductTypes,
    saveGeographicArea,
    validateProfile
  }
}

composables/useGeographySearch.ts

export interface State {
  id: number
  sigla: string
  nome: string
}

export interface City {
  id: number
  nome: string
  state_id: number
  state: State
}

export const useGeographySearch = () => {
  const config = useRuntimeConfig()

  const states = ref<State[]>([])
  const cities = ref<City[]>([])
  const loading = ref(false)

  const loadStates = async () => {
    try {
      loading.value = true
      const response = await $fetch<{ data: State[] }>(
        '/api/v1/geography/states',
        { baseURL: config.public.apiBase }
      )
      states.value = response.data
    } catch (e) {
      console.error('Erro ao carregar estados:', e)
    } finally {
      loading.value = false
    }
  }

  const searchCities = useDebounceFn(async (query: string) => {
    if (query.length < 3) {
      cities.value = []
      return []
    }

    try {
      loading.value = true
      const response = await $fetch<{ data: City[] }>(
        `/api/v1/geography/cities/search?q=${encodeURIComponent(query)}`,
        { baseURL: config.public.apiBase }
      )
      cities.value = response.data
      return response.data
    } catch (e) {
      console.error('Erro ao buscar cidades:', e)
      return []
    } finally {
      loading.value = false
    }
  }, 300)

  return {
    states,
    cities,
    loading,
    loadStates,
    searchCities
  }
}

✅ CRITÉRIOS DE VALIDAÇÃO

Perfil Completo = TRUE quando:

  1. E-mail Validado
    • Código de verificação enviado e confirmado
    • Campo email_verified_at não nulo na tabela user_accesses
  2. Pelo Menos 1 Endereço
    • Tabela addresses com addressable_type = 'App\Models\Supplier'
    • addressable_id = supplier.id
    • status = true
  3. Pelo Menos 1 Tipo de Produto
    • Tabela supplier_product_types com supplier_id = supplier.id
    • status = true
    • Mínimo 1 registro
  4. Pelo Menos 1 Item na Área de Atuação
    • OPÇÃO A: Scope nacional (flag no sistema, sem registros específicos)
    • OPÇÃO B: Mínimo 1 registro em supplier_states
    • OPÇÃO C: Mínimo 1 registro em supplier_state_cities

🎯 MÉTRICAS DE SUCESSO

Métricas a Acompanhar

  1. Taxa de Conclusão de Perfil
    • Fórmula: completed_profiles / total_signups * 100
    • Meta: > 80%
  2. Tempo Médio para Completar
    • Fórmula: AVG(completed_at - signup_at)
    • Meta: < 5 minutos
  3. Taxa de Abandono por Seção
    • Email: abandoned_at_email / started_email * 100
    • Endereços: abandoned_at_address / started_address * 100
    • Produtos: abandoned_at_products / started_products * 100
    • Geografia: abandoned_at_geography / started_geography * 100
    • Meta: < 15% em cada seção
  4. Taxa de Erros de Validação
    • Fórmula: validation_errors / save_attempts * 100
    • Meta: < 5%
  5. NPS de Usabilidade
    • Pergunta: "Foi fácil completar seu perfil?"
    • Meta: > 85% positivo

♿ ACESSIBILIDADE (WCAG 2.1 AA)

Checklist

  • Contraste de Cores: Mínimo 4.5:1 para textos, 3:1 para elementos interativos
  • Navegação por Teclado: Todos os elementos focáveis com Tab, ativação com Space/Enter
  • Screen Readers: Labels descritivos, aria-label em ícones, estados anunciados
  • Feedback Visual: Estados de hover, focus, error sempre visíveis
  • Touch Targets: Mínimo 44x44px em mobile
  • Responsividade: Layout adapta de 320px (mobile) até 1920px (desktop)
  • Semântica HTML: Tags corretas (button, input, label, section)

🚀 PLANO DE IMPLEMENTAÇÃO

Fase 1: Backend - Banco de Dados (1 dia)

  • Criar migration create_supplier_product_types_table
  • Criar model SupplierProductType
  • Adicionar relacionamento no model Supplier
  • Executar migration e testar relacionamentos

Fase 2: Backend - Controllers e APIs (2 dias)

  • Criar GeographyController com métodos:
    • getStates()
    • getCitiesByState()
    • searchCities()
  • Adicionar métodos no SupplierController:
    • getProductTypes()
    • updateProductTypes()
    • getGeographicArea()
    • updateGeographicArea()
    • validateProfile()
  • Criar rotas no routes/api.php
  • Testar todas as APIs via Postman/Insomnia

Fase 3: Frontend - Base da Página (1 dia)

  • Criar estrutura da página apps/supplier/pages/profile.vue
  • Implementar header com avatar e informações
  • Implementar barra de progresso (AGR-152)
  • Reutilizar componente de validação de e-mail do producer
  • Reutilizar componente de endereços do producer

Fase 4: Frontend - Tipos de Produtos (1 dia)

  • Criar componente components/profile/ProductTypesSelector.vue
  • Implementar seleção visual (checkboxes estilizados)
  • Integrar com API /api/v1/supplier/product-types
  • Validar mínimo 1 selecionado

Fase 5: Frontend - Área Geográfica (3 dias)

  • Criar composable composables/useGeographySearch.ts
  • Criar componente components/profile/GeographicAreaSelector.vue
  • Criar sub-componente GeographicStatesSelector.vue
  • Criar sub-componente GeographicCitiesAutocomplete.vue
  • Implementar fluxo guiado (Nacional → Estados → Cidades)
  • Implementar resumo visual
  • Integrar com APIs de geografia

Fase 6: Validação e Testes (1-2 dias)

  • Implementar validação completa de perfil
  • Testar fluxo completo de preenchimento
  • Testar navegação por teclado
  • Testar com screen reader (NVDA/VoiceOver)
  • Testar responsividade (mobile/tablet/desktop)
  • Ajustes finais de UX

Fase 7: Documentação (meio dia)

  • Documentar APIs no Scribe
  • Atualizar README com fluxo de perfil
  • Documentar componentes criados

📦 ENTREGÁVEIS

  1. ✅ Migration e Model SupplierProductType
  2. ✅ Controller GeographyController completo
  3. ✅ Métodos adicionados no SupplierController
  4. ✅ Rotas de API documentadas no Scribe
  5. ✅ Página /profile completa e funcional
  6. ✅ Composables useSupplierProfile e useGeographySearch
  7. ✅ Componentes de seleção de produtos e área geográfica
  8. ✅ Testes de usabilidade realizados
  9. ✅ Documentação atualizada

🔍 DEPENDÊNCIAS

Bibliotecas Necessárias (já instaladas)

  • ✅ Vue 3 / Nuxt 3
  • ✅ Tailwind CSS
  • ✅ Laravel 11
  • ✅ PostgreSQL 16

Novos Composables VueUse (se necessário)

  • useDebounceFn (para autocomplete) - já disponível no VueUse

📝 NOTAS TÉCNICAS

Performance

  1. Cache de Estados
    • Estados são fixos (27), cachear por 1 hora
    • Usar Cache::remember('states', 3600, fn() => State::all())
  2. Autocomplete de Cidades
    • Mínimo 3 caracteres para buscar
    • Debounce de 300ms no frontend
    • Limitar a 10 resultados por query
    • Index em state_cities.nome para busca rápida
  3. Validação de Perfil
    • Cachear resultado por 5 minutos
    • Invalidar cache ao salvar qualquer seção
    • Usar jobs em background para recalcular se necessário

Segurança

  1. Autorização
    • Apenas o próprio fornecedor pode editar seu perfil
    • Middleware auth:sanctum em todas as rotas
    • Validar supplier_id = auth()->user()->supplier_id
  2. Validação de Dados
    • Validar IDs de estados/cidades existem no banco
    • Validar mínimo 1 item em cada seção obrigatória
    • Sanitizar queries de busca (SQL injection)

📅 CRONOGRAMA ESTIMADO

FaseDescriçãoDiasResponsável
1Backend - Banco de Dados1Dev
2Backend - APIs2Dev
3Frontend - Base1Dev
4Frontend - Produtos1Dev
5Frontend - Geografia3Dev
6Testes1-2Dev + QA
7Documentação0.5Dev
TOTAL9-10 dias

✅ APROVAÇÃO

  • Proposta de UX aprovada
  • Fluxo de área geográfica validado
  • Estrutura de APIs definida
  • Cronograma aceito

Aprovado por: Gustavo Carneiro Data: 2026-01-14 Status: ✅ Pronto para Desenvolvimento


📚 REFERÊNCIAS

Copyright © 2026