Guias

Resumo da Implementação - Sistema de Favoritos para Fornecedores

Data: 2025-01-11 Feature: Sistema completo de favoritos e visualização de licitações urgentes

Resumo da Implementação - Sistema de Favoritos para Fornecedores

Data: 2025-01-11 Feature: Sistema completo de favoritos e visualização de licitações urgentes


📋 VISÃO GERAL

Implementação completa de um sistema de favoritos para fornecedores, permitindo:

  • Favoritar/desfavoritar licitações
  • Filtrar visualização apenas de licitações favoritas
  • Ver no dashboard as 10 licitações mais urgentes
  • Feedback visual imediato em todas as ações
  • Sincronização entre todas as páginas

🔧 BACKEND (API Laravel)

1. Banco de Dados

Migration: supplier_order_declines

Arquivo: database/migrations/2025_01_11_create_supplier_order_declines_table.php

Schema::create('supplier_order_declines', function (Blueprint $table) {
    $table->id();
    $table->foreignId('supplier_id')->constrained('fornecedores')->onDelete('cascade');
    $table->foreignId('order_id')->constrained('orders')->onDelete('cascade');
    $table->text('reason')->nullable();
    $table->timestamps();
    $table->unique(['supplier_id', 'order_id']);
});

Propósito: Armazenar licitações que o fornecedor declinou (para futura implementação).


2. Models

Model: SupplierOrderDecline

Arquivo: app/Models/SupplierOrderDecline.php

class SupplierOrderDecline extends Model
{
    protected $fillable = ['supplier_id', 'order_id', 'reason'];

    public function supplier() {
        return $this->belongsTo(Fornecedor::class, 'supplier_id');
    }

    public function order() {
        return $this->belongsTo(Order::class);
    }
}

Model Atualizado: FavoriteOrder

Arquivo: app/Models/FavoriteOrder.php

Ajustado para usar relacionamento correto:

public function supplier() {
    return $this->belongsTo(Fornecedor::class, 'supplier_id');
}

3. API Endpoints

Endpoint 1: POST /api/v1/supplier/orders/{id}/favorite

Arquivo: app/Http/Controllers/Api/FornecedorController.php (método favoritarLicitacao)

Funcionalidade: Toggle de favorito (adiciona se não existe, remove se existe)

Request:

  • Method: POST
  • URL: /api/v1/supplier/orders/{id}/favorite
  • Auth: Bearer Token (Sanctum)

Response Success (200):

{
    "message": "Licitação adicionada aos favoritos",
    "is_favorited": true
}

Comportamento:

  • Se já está favoritado: remove e retorna is_favorited: false
  • Se não está favoritado: adiciona e retorna is_favorited: true
  • Cria registro em favorite_orders com supplier_id e order_id

Endpoint 2: GET /api/v1/supplier/orders (atualizado)

Arquivo: app/Http/Controllers/Api/FornecedorController.php (método licitacoes)

Funcionalidade: Listagem de licitações com filtro de favoritas

Filtros adicionados:

  • favorites: boolean (true para mostrar apenas favoritas)

Comportamento:

  • Exclui automaticamente licitações declinadas pelo fornecedor
  • Se favorites=true: retorna apenas licitações favoritadas
  • Adiciona campo favorited (boolean) em cada licitação
  • Paginação: 15 por página (padrão)

Response:

{
    "data": [
        {
            "id": 1,
            "uuid": "abc-123",
            "number": "LIC-2025-001",
            "description": "...",
            "favorited": true,
            "dias_restantes": 5,
            ...
        }
    ],
    "meta": {
        "current_page": 1,
        "last_page": 3,
        "per_page": 15,
        "total": 45
    }
}

Endpoint 3: GET /api/v1/supplier/dashboard (atualizado)

Arquivo: app/Http/Controllers/Api/FornecedorController.php (método dashboard)

Funcionalidade: Dashboard com métricas e licitações urgentes

Mudanças:

  • Adiciona campo licitacoesUrgentes com até 10 licitações
  • Ordenadas por deadline_date ASC (mais urgentes primeiro)
  • Cada licitação inclui campo favorited (boolean)
  • Exclui licitações declinadas automaticamente
  • Calcula dias_restantes baseado em deadline_date

Response:

{
    "metricas": { ... },
    "ultimasPropostas": [ ... ],
    "licitacoesUrgentes": [
        {
            "id": 1,
            "uuid": "abc-123",
            "number": "LIC-2025-001",
            "description": "...",
            "deadline_date": "2025-01-15",
            "dias_restantes": 4,
            "total_itens": 5,
            "favorited": false
        }
    ]
}

💻 FRONTEND (Nuxt 3)

1. Types TypeScript

Arquivo: apps/supplier/types/fornecedor.ts

Tipos atualizados:

export interface LicitacaoUrgente {
  id: number
  uuid: string
  number: string
  description: string
  deadline_date: string
  dias_restantes: number
  favorited: boolean
  total_itens: number
}

export interface DashboardResponse {
  metricas: DashboardMetricas
  ultimasPropostas: UltimaProposta[]
  licitacoesUrgentes: LicitacaoUrgente[]  // NOVO
}

export interface Licitacao {
  // ...campos existentes
  favorited?: boolean  // ATUALIZADO
}

2. Composable Dashboard

Arquivo: apps/supplier/composables/useDashboardMetrics.ts

Mudanças:

interface DashboardState {
  metricas: DashboardMetricas | null
  ultimasPropostas: UltimaProposta[]
  licitacoesUrgentes: LicitacaoUrgente[]  // NOVO
  loading: boolean
  error: string | null
  lastFetch: number | null
}

// Captura licitacoesUrgentes da API
state.value.licitacoesUrgentes = response.data.licitacoesUrgentes || []

// Retorna computed
licitacoesUrgentes: computed(() => state.value.licitacoesUrgentes)

3. Dashboard Page

Arquivo: apps/supplier/pages/dashboard.vue

Seção Adicionada: "Últimas Licitações"

Features:

  • ✅ Mostra até 10 licitações mais urgentes
  • ✅ Cards com:
    • Número da licitação
    • Descrição (line-clamp-2)
    • Badge colorido de urgência (dias restantes)
    • Quantidade de itens
    • Botão favoritar (estrela)
    • Link "Ver detalhes"

Estados:

  • Loading: 3 skeletons animados
  • Vazio: Mensagem "Nenhuma licitação disponível"
  • Com dados: Cards renderizados

Badges de Urgência:

const getUrgencyColor = (dias: number) => {
  if (dias < 0) return 'bg-agrsis-error-50 text-agrsis-error-500'    // Expirada
  if (dias <= 3) return 'bg-agrsis-error-50 text-agrsis-error-500'   // Urgente
  if (dias <= 7) return 'bg-yellow-50 text-agrsis-warning-400'       // Atenção
  return 'bg-green-50 text-agrsis-success-400'                        // Normal
}

Função de Favoritar:

const toggleFavorito = async (licitacao: any) => {
  const uuid = licitacao.uuid || licitacao.id
  if (favoritoLoading.value[uuid]) return  // Previne double-click

  favoritoLoading.value[uuid] = true

  try {
    const response = await $api(`/api/v1/supplier/orders/${uuid}/favorite`, {
      method: 'POST'
    })

    if (response.data) {
      licitacao.favorited = response.data.is_favorited  // Update local
      toast.success(response.data.is_favorited
        ? 'Licitação adicionada aos favoritos'
        : 'Licitação removida dos favoritos')
    }
  } catch (error: any) {
    toast.error('Erro ao favoritar licitação')
  } finally {
    favoritoLoading.value[uuid] = false
  }
}

Botão Favoritar:

<button
  @click="toggleFavorito(licitacao)"
  :disabled="favoritoLoading[licitacao.uuid]"
  class="flex-shrink-0 p-2 text-gray-400 hover:text-agrsis-warning-400
         transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
>
  <StarSolidIcon v-if="licitacao.favorited" class="w-5 h-5 text-agrsis-warning-400" />
  <StarIcon v-else class="w-5 h-5" />
</button>

4. Orders Page (Lista de Licitações)

Arquivo: apps/supplier/pages/orders/index.vue

Funcionalidades Adicionadas:

  1. Filtro de Favoritas:
const filtros = reactive({
  // ...outros filtros
  apenasFavoritas: false  // NOVO
})

// Watcher que recarrega ao mudar filtro
watch(() => filtros.apenasFavoritas, () => {
  currentPage.value = 1
  carregarLicitacoes()
})
  1. Botão de Filtro no Toolbar:
<button
  @click="filtros.apenasFavoritas = !filtros.apenasFavoritas"
  :class="[
    'px-3 py-2 rounded-lg transition-all duration-200 text-sm font-medium flex items-center gap-2',
    filtros.apenasFavoritas
      ? 'bg-agrsis-warning-50 text-agrsis-warning-600 border-2 border-agrsis-warning-300'
      : 'bg-white text-agrsis-neutral-600 border border-agrsis-neutral-300 hover:bg-agrsis-neutral-50'
  ]"
>
  <svg class="w-5 h-5" :class="{ 'fill-current': filtros.apenasFavoritas }">
    <!-- Ícone de estrela -->
  </svg>
  Favoritas
  <span v-if="filtros.apenasFavoritas">✓</span>
</button>
  1. Ícone de Favoritar em Cada Card:
<div class="relative">  <!-- Card com position: relative -->

  <!-- Botão favoritar (absolute top-right) -->
  <button
    @click.stop="toggleFavorito(licitacao)"
    :disabled="favoritoLoading[licitacao.uuid]"
    class="absolute top-4 right-4 p-2 rounded-full hover:bg-agrsis-neutral-100
           transition-colors disabled:opacity-50"
  >
    <svg
      class="w-6 h-6"
      :class="isFavorito(licitacao)
        ? 'fill-agrsis-warning-400 text-agrsis-warning-400'
        : 'text-agrsis-neutral-400'"
      :fill="isFavorito(licitacao) ? 'currentColor' : 'none'"
    >
      <!-- Ícone de estrela -->
    </svg>
  </button>

  <!-- Título com padding para não sobrepor estrela -->
  <h3 class="text-xl font-bold text-agrsis-neutral-900 mb-4 pr-10">
    {{ licitacao.number }}
  </h3>

  <!-- Resto do card -->
</div>
  1. Função de Favoritar:
const toggleFavorito = async (licitacao: any) => {
  const uuid = licitacao.uuid || licitacao.id
  if (favoritoLoading.value[uuid]) return

  favoritoLoading.value[uuid] = true

  try {
    const response = await $api(`/api/v1/supplier/orders/${uuid}/favorite`, {
      method: 'POST'
    })

    if (response.data) {
      licitacao.favorited = response.data.is_favorited

      if (response.data.is_favorited) {
        toast.success('Licitação adicionada aos favoritos')
      } else {
        toast.success('Licitação removida dos favoritos')

        // Se desfavoritou enquanto filtro está ativo, recarrega
        if (filtros.apenasFavoritas) {
          await carregarLicitacoes()
        }
      }
    }
  } catch (error: any) {
    toast.error('Erro ao favoritar licitação')
  } finally {
    favoritoLoading.value[uuid] = false
  }
}
  1. Limpar Filtros (atualizado):
const limparFiltros = () => {
  filtros.categorias = []
  filtros.estado = ''
  filtros.raioKm = 100
  filtros.prazo = 'todos'
  filtros.aceitaSimilares = false
  filtros.aceitaEscalonada = false
  filtros.apenasFavoritas = false  // NOVO
  carregarLicitacoes()
}

🎨 DESIGN & UX

Cores Utilizadas:

/* Favoritos (amarelo/warning) */
bg-agrsis-warning-50       /* Fundo claro */
text-agrsis-warning-400    /* Texto médio */
text-agrsis-warning-600    /* Texto escuro */
border-agrsis-warning-300  /* Borda */

/* Urgência */
bg-agrsis-error-50         /* Vermelho claro */
text-agrsis-error-500      /* Vermelho escuro */
bg-yellow-50               /* Amarelo claro (atenção) */
bg-green-50                /* Verde claro (normal) */
text-agrsis-success-400    /* Verde */

Interações:

  • Hover: Estrela cinza → amarela (preview)
  • Active: Estrela preenchida amarela
  • Loading: Botão desabilitado com opacity 50%
  • Click: @click.stop para não acionar navegação do card

Feedback Visual:

  • ✅ Toast de sucesso ao favoritar
  • ✅ Toast de sucesso ao desfavoritar
  • ❌ Toast de erro em caso de falha
  • ⏳ Loading state durante requisição
  • 🔄 Auto-reload ao remover favorito (com filtro ativo)

📁 ARQUIVOS MODIFICADOS

Backend:

api/
├── database/migrations/
│   └── 2025_01_11_create_supplier_order_declines_table.php  [NOVO]
├── app/Models/
│   ├── SupplierOrderDecline.php                             [NOVO]
│   └── FavoriteOrder.php                                    [MODIFICADO]
└── app/Http/Controllers/Api/
    └── FornecedorController.php                             [MODIFICADO]
        ├── favoritarLicitacao() → atualizado (toggle)
        ├── licitacoes() → adicionado filtro favorites
        └── dashboard() → adicionado licitacoesUrgentes

Frontend:

apps/supplier/
├── types/
│   └── fornecedor.ts                                        [MODIFICADO]
│       ├── LicitacaoUrgente (interface)
│       └── DashboardResponse (atualizado)
├── composables/
│   └── useDashboardMetrics.ts                               [MODIFICADO]
│       └── licitacoesUrgentes (adicionado)
└── pages/
    ├── dashboard.vue                                         [MODIFICADO]
    │   ├── Seção "Últimas Licitações" (nova)
    │   └── toggleFavorito() (implementado)
    └── orders/
        └── index.vue                                         [MODIFICADO]
            ├── Filtro "Favoritas" (toolbar)
            ├── Ícones de estrela (cards)
            └── toggleFavorito() (implementado)

✅ FUNCIONALIDADES IMPLEMENTADAS

  • Migration supplier_order_declines
  • Model SupplierOrderDecline com relacionamentos
  • Model FavoriteOrder corrigido
  • Endpoint POST /supplier/orders/{id}/favorite (toggle)
  • Endpoint GET /supplier/orders com filtro favorites
  • Endpoint GET /supplier/dashboard com licitacoesUrgentes
  • Exclusão automática de licitações declinadas
  • Dashboard: seção "Últimas Licitações"
  • Dashboard: botões de favoritar funcionais
  • Dashboard: badges de urgência coloridos
  • Orders: filtro "Favoritas" no toolbar
  • Orders: ícones de favoritar em cada card
  • Orders: auto-reload ao remover favorito
  • Types TypeScript atualizados
  • Loading states para prevenir double-click
  • Toast notifications para feedback
  • Sincronização entre páginas

⚠️ PENDÊNCIAS & PRÓXIMOS PASSOS

Alta Prioridade:

  • Renomear métodos PT → EN no FornecedorController
    • licitacoes()orders()
    • favoritarLicitacao()toggleFavorite()
    • licitacaoDetalhes()orderDetails()
    • propostas()proposals()
    • etc.

Média Prioridade:

  • Implementar endpoint de declinar licitação
  • Testes automatizados (PHPUnit + Vitest)
  • Documentação de API (Swagger/OpenAPI)
  • Otimizações de performance (N+1 queries)

Baixa Prioridade:

  • Cache de favorites no frontend
  • Animações de transição
  • Filtros avançados (múltiplos critérios)
  • Export de favoritas (CSV/PDF)

🧪 TESTES

Consulte o arquivo CHECKLIST-TESTES-FAVORITOS.md para:

  • Testes funcionais (passo a passo)
  • Testes de integração
  • Testes de API
  • Edge cases
  • Critérios de sucesso

Script de teste de API: test-favorites-flow.sh


🚀 COMO USAR

Para o Desenvolvedor:

  1. Iniciar servidores:
# Terminal 1: API Laravel
cd apps/api
DB_HOST=127.0.0.1 DB_PORT=5433 php artisan serve --port=8000

# Terminal 2: Frontend Nuxt
cd apps/supplier
npm run dev
  1. Acessar:
  1. Testar:
  • Seguir checklist em CHECKLIST-TESTES-FAVORITOS.md
  • Executar script test-favorites-flow.sh (após obter token)

Para o Usuário Final (Fornecedor):

  1. Fazer login no portal do fornecedor
  2. Dashboard: Ver licitações urgentes com badges coloridos
  3. Favoritar: Clicar na estrela de qualquer licitação
  4. Filtrar: Ir em "Licitações" > clicar "Favoritas"
  5. Desfavoritar: Clicar novamente na estrela preenchida

📊 ESTATÍSTICAS

  • Backend: 3 arquivos criados, 3 modificados
  • Frontend: 3 arquivos modificados
  • Endpoints: 3 (1 novo, 2 atualizados)
  • Linhas de código: ~800 linhas
  • Funcionalidades: 15+ features implementadas
  • Tempo estimado: ~8 horas de desenvolvimento

📝 NOTAS FINAIS

Decisões de Design:

  1. Toggle em vez de Add/Remove separados:
    • Mais simples para o usuário
    • Menos endpoints para manter
    • UX mais intuitiva
  2. Auto-reload ao desfavoritar com filtro ativo:
    • Feedback imediato
    • Evita confusão do usuário
    • UX mais fluida
  3. Ordenação por urgência (deadline):
    • Prioriza licitações que estão prestes a expirar
    • Ajuda fornecedor a não perder oportunidades
    • Alinhado com objetivo de negócio
  4. Badges coloridos:
    • Feedback visual rápido
    • Hierarquia de urgência clara
    • Cores intuitivas (vermelho = urgente, verde = normal)
  5. Exclusão automática de declinadas:
    • Mantém lista limpa
    • Reduz ruído para o usuário
    • Prepara infraestrutura para feature futura

Melhorias Futuras:

  • Notificações push para licitações urgentes
  • Ordenação customizada (preço, distância, etc.)
  • Salvar preferências de filtros
  • Compartilhar favoritas com equipe
  • Export de relatórios

Documento criado: 2025-01-11 Última atualização: 2025-01-11 Status: ✅ Implementação Completa (exceto renomear métodos PT→EN)

Copyright © 2026