Supplier Portal

🔄 PLANO DE REVISÃO COMPLETA - Portal do Fornecedor

Data: 11 de Janeiro de 2025 Versão: 2.0 Status: 🚧 Em Implementação

🔄 PLANO DE REVISÃO COMPLETA - Portal do Fornecedor

Data: 11 de Janeiro de 2025 Versão: 2.0 Status: 🚧 Em Implementação


📋 Sumário Executivo

Este documento detalha a revisão completa do frontend do Portal do Fornecedor, corrigindo problemas identificados e implementando funcionalidades críticas para o modelo de negócio do AgrSis.

Problemas Críticos Identificados:

  1. Anonimato violado: Dados do produtor expostos antes da seleção
  2. Dashboard mockado: Métricas não refletem dados reais
  3. Filtros inadequados: "Valor Estimado" não existe no modelo de dados
  4. ⚠️ Proposta incompleta: Falta observações, desconto e entrega escalonada
  5. ⚠️ Sem opção de produtos similares: Funcionalidade não implementada

🎯 Princípios do Modelo de Negócio

Regra de Ouro: Anonimato da Licitação

O fornecedor NÃO deve saber quem é o produtor até que sua proposta seja selecionada.

Por quê?

  • Evita favorecimentos baseados em relacionamentos pessoais
  • Garante competição justa baseada apenas em preço/qualidade
  • Protege o produtor de assédio comercial
  • Aumenta a confiança na plataforma

O que pode ser mostrado:

  • ✅ Número da licitação (anônimo)
  • ✅ Descrição dos produtos
  • ✅ Quantidades solicitadas
  • ✅ Raio de fornecimento
  • ✅ Prazo de entrega
  • ✅ Localização (cidade/UF apenas)
  • ✅ Se aceita produtos similares
  • ✅ Se aceita entrega escalonada

O que NÃO pode ser mostrado:

  • ❌ Nome do produtor
  • ❌ CPF/CNPJ do produtor
  • ❌ Endereço completo
  • ❌ Telefone/email
  • ❌ Histórico de compras
  • ❌ Foto/logo da propriedade

📊 Análise Detalhada por Tela

1. Dashboard (/)

✅ Estado Atual:

  • Layout e UX bem estruturados
  • Métricas definidas
  • Responsivo

❌ Problemas:

// composables/useDashboardFornecedor.ts - Linha ~15
const metricas = ref({
  licitacoesDisponiveis: 0,        // ❌ Hardcoded
  propostasEnviadas: 0,             // ❌ Hardcoded
  propostasVencedoras: 0,           // ❌ Hardcoded
  valorTotalNegociado: 0,           // ❌ Hardcoded
  // ... variações também hardcoded
})

🔧 Solução:

// Integração real com API
const carregarMetricas = async () => {
  try {
    const { data } = await fornecedorApi.getDashboard()

    metricas.value = {
      licitacoesDisponiveis: data.licitacoes_disponiveis,
      licitacoesDisponiveisVariacao: data.licitacoes_disponiveis_variacao,
      propostasEnviadas: data.propostas_enviadas,
      propostasEnviadasVariacao: data.propostas_enviadas_variacao,
      propostasVencedoras: data.propostas_vencedoras,
      propostasVencedorasVariacao: data.propostas_vencedoras_variacao,
      valorTotalNegociado: data.valor_total_negociado,
      valorTotalNegociadoVariacao: data.valor_total_negociado_variacao
    }
  } catch (error) {
    toast.error('Erro ao carregar métricas')
  }
}

📍 Endpoint Backend Necessário:

// app/Http/Controllers/Api/FornecedorController.php

public function dashboard(Request $request)
{
    $fornecedor = $request->user()->fornecedor;

    // Período de comparação (30 dias)
    $periodoAtual = now()->subDays(30);
    $periodoAnterior = now()->subDays(60);

    // Licitações disponíveis (dentro do raio)
    $licitacoesDisponiveis = Order::published()
        ->withinSupplyRadius($fornecedor)
        ->whereDoesntHave('proposals', fn($q) =>
            $q->where('supplier_id', $fornecedor->id)
        )
        ->count();

    $licitacoesDisponiveisAnterior = Order::published()
        ->whereBetween('created_at', [$periodoAnterior, $periodoAtual])
        ->withinSupplyRadius($fornecedor)
        ->count();

    // Propostas enviadas
    $propostasEnviadas = Proposal::where('supplier_id', $fornecedor->id)
        ->where('created_at', '>=', $periodoAtual)
        ->count();

    $propostasEnviadasAnterior = Proposal::where('supplier_id', $fornecedor->id)
        ->whereBetween('created_at', [$periodoAnterior, $periodoAtual])
        ->count();

    // Propostas vencedoras
    $propostasVencedoras = Proposal::where('supplier_id', $fornecedor->id)
        ->where('status', 'accepted')
        ->where('created_at', '>=', $periodoAtual)
        ->count();

    // Valor total negociado
    $valorTotal = Proposal::where('supplier_id', $fornecedor->id)
        ->where('status', 'accepted')
        ->where('created_at', '>=', $periodoAtual)
        ->sum('total_amount');

    return response()->json([
        'licitacoes_disponiveis' => $licitacoesDisponiveis,
        'licitacoes_disponiveis_variacao' => $licitacoesDisponiveis - $licitacoesDisponiveisAnterior,
        'propostas_enviadas' => $propostasEnviadas,
        'propostas_enviadas_variacao' => $propostasEnviadas - $propostasEnviadasAnterior,
        'propostas_vencedoras' => $propostasVencedoras,
        'propostas_vencedoras_variacao' => '15%', // Calcular taxa
        'valor_total_negociado' => $valorTotal,
        'valor_total_negociado_variacao' => 'R$ ' . number_format($valorTotal * 0.25, 2)
    ]);
}

2. Listagem de Licitações (/orders)

❌ Problemas Identificados:

Problema 1: Exposição de Dados do Produtor

<!-- Linha 750 - pages/orders/index.vue -->
<span>{{ licitacao.produtor.nome }}</span>  <!-- ❌ VIOLA ANONIMATO -->

Problema 2: Filtros Inadequados

<!-- Linhas 429-454 - Filtro "Valor Estimado" -->
<div>
  <h3 class="label mb-3">Valor Estimado</h3>  <!-- ❌ Não existe no modelo -->
  <div class="space-y-3">
    <input v-model.number="filtros.valorMin" />
    <input v-model.number="filtros.valorMax" />
  </div>
</div>

🔧 Solução:

1. Remover dados do produtor:

<!-- ANTES -->
<div class="flex items-center gap-2">
  <span>📍</span>
  <span>{{ licitacao.produtor.nome }}</span>  <!-- ❌ -->
  <span>({{ licitacao.raio_fornecimento }}km)</span>
</div>

<!-- DEPOIS -->
<div class="flex items-center gap-2">
  <span>📍</span>
  <span>{{ licitacao.endereco.cidade }}/{{ licitacao.endereco.uf }}</span>  <!-- ✅ -->
  <span class="text-xs text-agrsis-neutral-500">
    ({{ licitacao.supply_radius }}km de você)
  </span>
</div>

2. Remover filtro de "Valor Estimado":

<!-- REMOVER COMPLETAMENTE SEÇÃO DE VALOR -->

<!-- ADICIONAR FILTROS REAIS: -->
<div>
  <h3 class="label mb-3">Status</h3>
  <div class="space-y-2">
    <label class="flex items-center gap-2 cursor-pointer">
      <input v-model="filtros.status" type="radio" value="ativa" class="radio" />
      <span class="text-sm text-agrsis-neutral-700">Ativas</span>
    </label>
    <label class="flex items-center gap-2 cursor-pointer">
      <input v-model="filtros.status" type="radio" value="minhas" class="radio" />
      <span class="text-sm text-agrsis-neutral-700">Onde enviei proposta</span>
    </label>
  </div>
</div>

<div class="divider"></div>

<div>
  <h3 class="label mb-3">Aceita Produtos Similares</h3>
  <label class="flex items-center gap-2 cursor-pointer">
    <input v-model="filtros.aceitaSimilares" type="checkbox" class="checkbox" />
    <span class="text-sm text-agrsis-neutral-700">Apenas licitações que aceitam</span>
  </label>
</div>

<div class="divider"></div>

<div>
  <h3 class="label mb-3">Entrega Escalonada</h3>
  <label class="flex items-center gap-2 cursor-pointer">
    <input v-model="filtros.aceitaEscalonada" type="checkbox" class="checkbox" />
    <span class="text-sm text-agrsis-neutral-700">Apenas licitações que aceitam</span>
  </label>
</div>

3. Atualizar filtros no composable:

// composables/useFornecedor.ts
const filtros = reactive({
  categorias: [] as string[],
  estado: '',
  raioKm: 100,
  prazo: 'todos',
  status: 'ativa', // ativa | minhas
  aceitaSimilares: false,
  aceitaEscalonada: false
})

const listarLicitacoes = async (params?: any) => {
  const queryParams = {
    status: filtros.status,
    search: params?.search,
    estado: filtros.estado || undefined,
    raio_max: filtros.raioKm,
    aceita_similares: filtros.aceitaSimilares || undefined,
    aceita_escalonada: filtros.aceitaEscalonada || undefined,
    page: params?.page || 1,
    per_page: params?.per_page || 12
  }

  const { data, error } = await fornecedorApi.getLicitacoes(queryParams)

  if (!error && data) {
    licitacoes.value = data.data
    paginationMeta.value = data.meta
  }
}

3. Detalhes da Licitação (/orders/[id])

❌ Problemas Identificados:

Problema 1: Nome do Produtor Exposto

<!-- Linha 273 - pages/orders/[id].vue -->
<span>Produtor: <strong>{{ licitacao.produtor.nome }}</strong></span>  <!-- ❌ -->

Problema 2: Endereço Completo Exposto

<!-- Linhas 376-406 - Endereço completo -->
<p>{{ licitacao.produtor.endereco.logradouro }}, {{ licitacao.produtor.endereco.numero }}</p>
<p>{{ licitacao.produtor.endereco.bairro }}</p>
<p>{{ licitacao.produtor.endereco.cidade }}/{{ licitacao.produtor.endereco.uf }}</p>

🔧 Solução:

1. Remover identificação do produtor:

<!-- ANTES -->
<div class="flex items-center gap-2 text-sm text-gray-500">
  <svg>...</svg>
  <span>Produtor: <strong>{{ licitacao.produtor.nome }}</strong></span>  <!-- ❌ -->
</div>

<!-- DEPOIS -->
<div class="flex items-center gap-2 text-sm text-gray-500">
  <svg>...</svg>
  <span>Licitação: <strong>{{ licitacao.number }}</strong></span>  <!-- ✅ -->
</div>

2. Mostrar apenas cidade/UF:

<!-- Logística e Entrega -->
<section class="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
  <h2 class="text-lg font-bold text-gray-900 mb-5 flex items-center gap-2">
    <svg>...</svg>
    Logística e Entrega
  </h2>

  <!-- Localização (sem endereço completo) -->
  <div class="bg-blue-50 border border-blue-200 rounded-lg p-4">
    <h3 class="text-sm font-semibold text-blue-900 mb-3">Região de Entrega</h3>

    <div class="flex items-start gap-2">
      <svg class="w-5 h-5 text-blue-600 mt-0.5">...</svg>
      <div>
        <p class="text-base font-semibold text-gray-900">
          {{ licitacao.endereco.cidade }} / {{ licitacao.endereco.uf }}
        </p>
        <p class="text-sm text-gray-600 mt-1">
          Raio de fornecimento: <strong>{{ licitacao.supply_radius }} km</strong>
        </p>
      </div>
    </div>
  </div>

  <!-- Informações de Entrega -->
  <div class="mt-4 space-y-3">
    <div class="flex items-center justify-between py-2 border-b border-gray-100">
      <span class="text-sm text-gray-600">Prazo de Entrega</span>
      <span class="text-sm font-semibold text-gray-900">
        Até {{ formatData(licitacao.delivery_deadline) }}
      </span>
    </div>

    <div class="flex items-center justify-between py-2 border-b border-gray-100">
      <span class="text-sm text-gray-600">Aceita Produtos Similares</span>
      <span :class="licitacao.accepts_similar_products ? 'badge-success' : 'badge-neutral'">
        {{ licitacao.accepts_similar_products ? 'Sim' : 'Não' }}
      </span>
    </div>

    <div class="flex items-center justify-between py-2">
      <span class="text-sm text-gray-600">Aceita Entrega Escalonada</span>
      <span :class="licitacao.accepts_staggered_delivery ? 'badge-success' : 'badge-neutral'">
        {{ licitacao.accepts_staggered_delivery ? 'Sim' : 'Não' }}
      </span>
    </div>
  </div>
</section>

4. Envio de Proposta (/orders/[id]/proposal)

⚠️ Estado Atual:

  • ✅ Tela única (sem wizard)
  • ✅ Cálculo automático de subtotais
  • ❌ Falta campo de observações
  • ❌ Falta campo de desconto
  • ❌ Falta opção de produtos similares
  • ❌ Falta entrega escalonada

🔧 Solução Completa:

Interface TypeScript Atualizada:

// types/fornecedor.ts

interface PropostaItem {
  order_item_id: number
  produto_nome: string
  marca?: string
  quantidade: number
  unidade: string
  preco_unitario: number | null
  valor_total: number

  // NOVO: Produto Similar
  produto_similar?: {
    produto_id: number
    nome: string
    marca: string
    motivo: string // Por que é similar
  }
  usa_produto_similar: boolean
}

interface PropostaFormData {
  order_id: string
  items: PropostaItem[]

  // NOVO: Desconto e Observações
  subtotal: number
  desconto_percentual: number // 0-100
  desconto_valor: number // Calculado
  total_final: number
  observacoes: string // Máx 1000 caracteres

  // NOVO: Entrega Escalonada
  aceita_entrega_escalonada: boolean
  entregas_escalonadas?: EntregaEscalonada[]
}

interface EntregaEscalonada {
  ordem: number // 1, 2, 3...
  data_prevista: string // YYYY-MM-DD
  percentual: number // 0-100
  quantidade_items: { [order_item_id: number]: number }
  observacao?: string
}

Layout da Tela:

<template>
  <div class="min-h-screen bg-gray-50">
    <div class="container mx-auto px-4 py-8">

      <!-- Header com Info da Licitação -->
      <section class="bg-white rounded-lg shadow-sm border p-6 mb-6">
        <h1 class="text-3xl font-bold text-gray-900 mb-2">
          Enviar Proposta - {{ licitacao.number }}
        </h1>
        <p class="text-gray-600">{{ licitacao.description }}</p>

        <!-- Deadline Indicator -->
        <div class="mt-4 flex items-center gap-3">
          <div :class="prazoClass" class="px-4 py-2 rounded-lg border-2">
            <p class="text-xs font-semibold uppercase tracking-wide">
              Prazo para Propostas
            </p>
            <p class="text-xl font-bold">{{ textoPrazo }}</p>
            <p class="text-xs">{{ formatarData(licitacao.deadline_date) }}</p>
          </div>

          <!-- Badges -->
          <div class="flex gap-2">
            <span v-if="licitacao.accepts_similar_products" class="badge-success">
              ✓ Aceita Similares
            </span>
            <span v-if="licitacao.accepts_staggered_delivery" class="badge-info">
              ✓ Aceita Escalonada
            </span>
          </div>
        </div>
      </section>

      <!-- Grid: 2/3 + 1/3 -->
      <div class="grid grid-cols-1 lg:grid-cols-3 gap-6">

        <!-- COLUNA PRINCIPAL (2 colunas) -->
        <div class="lg:col-span-2 space-y-6">

          <!-- SEÇÃO 1: Produtos e Preços -->
          <section class="bg-white rounded-lg shadow-sm border p-6">
            <h2 class="text-xl font-bold mb-4">Produtos e Preços</h2>

            <!-- Desktop: Tabela -->
            <div class="overflow-x-auto">
              <table class="w-full">
                <thead class="bg-gray-50 border-b-2 border-gray-200">
                  <tr>
                    <th class="px-4 py-3 text-left text-xs font-semibold uppercase">
                      Produto
                    </th>
                    <th class="px-4 py-3 text-center text-xs font-semibold uppercase">
                      Quantidade
                    </th>
                    <th class="px-4 py-3 text-left text-xs font-semibold uppercase">
                      Preço Unit.
                    </th>
                    <th class="px-4 py-3 text-right text-xs font-semibold uppercase">
                      Subtotal
                    </th>
                  </tr>
                </thead>
                <tbody class="divide-y divide-gray-100">
                  <tr v-for="(item, index) in form.items" :key="item.order_item_id">
                    <!-- Produto -->
                    <td class="px-4 py-4">
                      <div>
                        <p class="font-semibold text-gray-900">{{ item.produto_nome }}</p>
                        <p v-if="item.marca" class="text-xs text-gray-500">
                          Marca: {{ item.marca }}
                        </p>

                        <!-- NOVO: Produto Similar -->
                        <div v-if="licitacao.accepts_similar_products" class="mt-2">
                          <button
                            @click="abrirModalProdutoSimilar(index)"
                            class="text-xs text-blue-600 hover:text-blue-700 flex items-center gap-1"
                          >
                            <svg class="w-4 h-4">...</svg>
                            {{ item.usa_produto_similar ? 'Produto Similar Selecionado' : 'Oferecer Similar' }}
                          </button>

                          <!-- Preview do Similar -->
                          <div v-if="item.usa_produto_similar && item.produto_similar"
                               class="mt-2 p-2 bg-blue-50 border border-blue-200 rounded text-xs">
                            <p class="font-medium text-blue-900">
                              {{ item.produto_similar.nome }}
                            </p>
                            <p class="text-blue-700">{{ item.produto_similar.marca }}</p>
                            <p class="text-blue-600 italic">{{ item.produto_similar.motivo }}</p>
                          </div>
                        </div>
                      </div>
                    </td>

                    <!-- Quantidade -->
                    <td class="px-4 py-4 text-center">
                      <p class="font-medium">{{ item.quantidade }} {{ item.unidade }}</p>
                    </td>

                    <!-- Preço Unitário -->
                    <td class="px-4 py-4">
                      <div class="relative">
                        <span class="absolute left-3 top-1/2 -translate-y-1/2 text-gray-500">R$</span>
                        <input
                          v-model.number="item.preco_unitario"
                          @input="calcularTotais"
                          type="number"
                          step="0.01"
                          min="0"
                          required
                          class="w-full pl-10 pr-4 py-2.5 border rounded-lg"
                          :class="{
                            'border-gray-300': item.preco_unitario && item.preco_unitario > 0,
                            'border-red-300 bg-red-50': !item.preco_unitario || item.preco_unitario <= 0
                          }"
                        />
                      </div>
                    </td>

                    <!-- Subtotal -->
                    <td class="px-4 py-4 text-right">
                      <p class="font-bold text-gray-900">
                        {{ formatCurrency(item.valor_total) }}
                      </p>
                    </td>
                  </tr>
                </tbody>
              </table>
            </div>
          </section>

          <!-- SEÇÃO 2: Desconto e Observações -->
          <section class="bg-white rounded-lg shadow-sm border p-6">
            <h2 class="text-xl font-bold mb-4">Condições Comerciais</h2>

            <div class="space-y-6">
              <!-- Desconto -->
              <div>
                <label class="block text-sm font-medium text-gray-700 mb-2">
                  Desconto (%)
                </label>
                <div class="flex items-center gap-4">
                  <input
                    v-model.number="form.desconto_percentual"
                    @input="calcularTotais"
                    type="number"
                    min="0"
                    max="100"
                    step="0.1"
                    class="input w-32"
                    placeholder="0.00"
                  />
                  <span class="text-sm text-gray-600">
                    = {{ formatCurrency(form.desconto_valor) }} de desconto
                  </span>
                </div>
                <p class="text-xs text-gray-500 mt-1">
                  Aplicado sobre o valor total dos produtos
                </p>
              </div>

              <!-- Observações -->
              <div>
                <label class="block text-sm font-medium text-gray-700 mb-2">
                  Observações
                </label>
                <textarea
                  v-model="form.observacoes"
                  rows="4"
                  maxlength="1000"
                  class="input w-full"
                  placeholder="Adicione informações relevantes: condições de pagamento, prazo de entrega, garantias, etc."
                ></textarea>
                <div class="flex justify-between items-center mt-1">
                  <p class="text-xs text-gray-500">
                    Máximo 1000 caracteres
                  </p>
                  <p class="text-xs" :class="form.observacoes.length > 900 ? 'text-red-600' : 'text-gray-500'">
                    {{ form.observacoes.length }} / 1000
                  </p>
                </div>
              </div>
            </div>
          </section>

          <!-- SEÇÃO 3: Entrega Escalonada (Condicional) -->
          <section v-if="licitacao.accepts_staggered_delivery"
                   class="bg-white rounded-lg shadow-sm border p-6">
            <div class="flex items-center justify-between mb-4">
              <h2 class="text-xl font-bold">Entrega Escalonada</h2>
              <label class="flex items-center gap-2">
                <input
                  v-model="form.aceita_entrega_escalonada"
                  type="checkbox"
                  class="checkbox"
                />
                <span class="text-sm font-medium text-gray-700">
                  Oferecer entrega em etapas
                </span>
              </label>
            </div>

            <!-- Configuração de Entregas -->
            <div v-if="form.aceita_entrega_escalonada" class="space-y-4">
              <div class="p-4 bg-blue-50 border border-blue-200 rounded-lg">
                <p class="text-sm text-blue-900">
                  Configure as datas e quantidades para cada etapa de entrega.
                  A soma dos percentuais deve ser 100%.
                </p>
              </div>

              <!-- Lista de Entregas -->
              <div v-for="(entrega, idx) in form.entregas_escalonadas"
                   :key="idx"
                   class="border border-gray-200 rounded-lg p-4">
                <div class="flex items-center justify-between mb-3">
                  <h3 class="font-semibold text-gray-900">
                    Etapa {{ idx + 1 }}
                  </h3>
                  <button
                    v-if="form.entregas_escalonadas.length > 1"
                    @click="removerEntrega(idx)"
                    class="text-red-600 hover:text-red-700 text-sm"
                  >
                    Remover
                  </button>
                </div>

                <div class="grid grid-cols-2 gap-4">
                  <!-- Data Prevista -->
                  <div>
                    <label class="block text-sm font-medium text-gray-700 mb-1">
                      Data Prevista
                    </label>
                    <input
                      v-model="entrega.data_prevista"
                      type="date"
                      :min="getDataMinimaEntrega()"
                      required
                      class="input w-full"
                    />
                  </div>

                  <!-- Percentual -->
                  <div>
                    <label class="block text-sm font-medium text-gray-700 mb-1">
                      Percentual (%)
                    </label>
                    <input
                      v-model.number="entrega.percentual"
                      @input="calcularQuantidadesEscalonadas(idx)"
                      type="number"
                      min="1"
                      max="100"
                      step="1"
                      required
                      class="input w-full"
                    />
                  </div>
                </div>

                <!-- Quantidades por Produto -->
                <div class="mt-4 space-y-2">
                  <p class="text-sm font-medium text-gray-700">
                    Quantidades nesta etapa:
                  </p>
                  <div v-for="item in form.items" :key="item.order_item_id"
                       class="flex justify-between items-center text-sm py-1 border-b border-gray-100">
                    <span class="text-gray-600">{{ item.produto_nome }}</span>
                    <span class="font-medium text-gray-900">
                      {{ calcularQuantidadeEtapa(item, entrega.percentual) }} {{ item.unidade }}
                    </span>
                  </div>
                </div>

                <!-- Observação da Etapa -->
                <div class="mt-4">
                  <label class="block text-sm font-medium text-gray-700 mb-1">
                    Observação (opcional)
                  </label>
                  <input
                    v-model="entrega.observacao"
                    type="text"
                    maxlength="200"
                    placeholder="Ex: Primeira parcela disponível imediatamente"
                    class="input w-full"
                  />
                </div>
              </div>

              <!-- Botão Adicionar Etapa -->
              <button
                @click="adicionarEntrega"
                :disabled="form.entregas_escalonadas.length >= 5"
                class="btn-outline w-full"
              >
                + Adicionar Etapa de Entrega
              </button>

              <!-- Validação de Percentual -->
              <div v-if="totalPercentualEscalonado !== 100"
                   class="p-3 bg-yellow-50 border border-yellow-200 rounded-lg">
                <p class="text-sm text-yellow-900">
                  ⚠️ Total atual: {{ totalPercentualEscalonado }}%
                  (deve ser exatamente 100%)
                </p>
              </div>
            </div>
          </section>

        </div>

        <!-- SIDEBAR (1 coluna) -->
        <aside class="lg:col-span-1">
          <div class="bg-white rounded-lg shadow-sm border p-6 sticky top-6">
            <h3 class="text-lg font-bold text-gray-900 mb-4">
              Resumo da Proposta
            </h3>

            <!-- Estatísticas -->
            <div class="space-y-3 mb-6">
              <div class="flex justify-between py-2 border-b border-gray-100">
                <span class="text-sm text-gray-600">Produtos cotados</span>
                <span class="font-semibold text-gray-900">
                  {{ produtosCotados }} / {{ form.items.length }}
                </span>
              </div>

              <div class="flex justify-between py-2 border-b border-gray-100">
                <span class="text-sm text-gray-600">Subtotal</span>
                <span class="font-semibold text-gray-900">
                  {{ formatCurrency(form.subtotal) }}
                </span>
              </div>

              <div v-if="form.desconto_valor > 0"
                   class="flex justify-between py-2 border-b border-gray-100">
                <span class="text-sm text-green-600">
                  Desconto ({{ form.desconto_percentual }}%)
                </span>
                <span class="font-semibold text-green-600">
                  - {{ formatCurrency(form.desconto_valor) }}
                </span>
              </div>

              <div class="flex justify-between py-3 pt-3 border-t-2 border-gray-200">
                <span class="text-base font-medium text-gray-900">Total Final</span>
                <span class="text-2xl font-bold text-agrsis-primary-700">
                  {{ formatCurrency(form.total_final) }}
                </span>
              </div>
            </div>

            <!-- Barra de Progresso -->
            <div class="mb-6">
              <div class="flex justify-between items-center mb-2">
                <span class="text-xs font-medium text-gray-600">Progresso</span>
                <span class="text-xs font-bold text-agrsis-primary-700">
                  {{ Math.round((produtosCotados / form.items.length) * 100) }}%
                </span>
              </div>
              <div class="w-full h-2 bg-gray-200 rounded-full overflow-hidden">
                <div
                  class="h-full bg-agrsis-primary-600 transition-all duration-500 rounded-full"
                  :style="{ width: `${(produtosCotados / form.items.length) * 100}%` }"
                />
              </div>
            </div>

            <!-- Botão de Envio -->
            <button
              @click="enviarProposta"
              :disabled="!formValido || isSubmitting"
              class="w-full py-4 px-6 rounded-lg font-semibold text-white transition-all min-h-[56px]"
              :class="{
                'bg-agrsis-primary-600 hover:bg-agrsis-primary-700 hover:shadow-lg transform hover:-translate-y-0.5': formValido && !isSubmitting,
                'bg-gray-300 cursor-not-allowed': !formValido || isSubmitting
              }"
            >
              <span v-if="!isSubmitting">✓ Enviar Proposta</span>
              <span v-else class="flex items-center justify-center gap-2">
                <svg class="w-5 h-5 animate-spin">...</svg>
                Enviando...
              </span>
            </button>

            <!-- Informações Adicionais -->
            <div class="mt-6 pt-6 border-t border-gray-200 space-y-3">
              <div class="flex items-start gap-2 text-xs text-gray-600">
                <svg class="w-4 h-4 text-green-600 mt-0.5">...</svg>
                <p>Sua proposta será enviada imediatamente</p>
              </div>
              <div class="flex items-start gap-2 text-xs text-gray-600">
                <svg class="w-4 h-4 text-blue-600 mt-0.5">...</svg>
                <p>Você será notificado sobre atualizações</p>
              </div>
            </div>
          </div>
        </aside>

      </div>
    </div>
  </div>

  <!-- Modal: Produto Similar -->
  <ModalProdutoSimilar
    v-model:show="showModalSimilar"
    :item="itemSelecionado"
    @confirmar="selecionarProdutoSimilar"
  />
</template>

Lógica do Composable:

// composables/usePropostaForm.ts

export const usePropostaForm = (licitacaoId: string) => {
  const form = reactive<PropostaFormData>({
    order_id: licitacaoId,
    items: [],
    subtotal: 0,
    desconto_percentual: 0,
    desconto_valor: 0,
    total_final: 0,
    observacoes: '',
    aceita_entrega_escalonada: false,
    entregas_escalonadas: []
  })

  // Cálculo de totais
  const calcularTotais = () => {
    // 1. Subtotal (soma de todos os itens)
    form.subtotal = form.items.reduce((sum, item) => {
      item.valor_total = (item.preco_unitario || 0) * item.quantidade
      return sum + item.valor_total
    }, 0)

    // 2. Desconto
    form.desconto_valor = (form.subtotal * form.desconto_percentual) / 100

    // 3. Total final
    form.total_final = form.subtotal - form.desconto_valor
  }

  // Produtos cotados
  const produtosCotados = computed(() => {
    return form.items.filter(item => item.preco_unitario && item.preco_unitario > 0).length
  })

  // Validação
  const formValido = computed(() => {
    // Todos os preços preenchidos
    if (produtosCotados.value !== form.items.length) return false

    // Se entrega escalonada, validar
    if (form.aceita_entrega_escalonada) {
      if (!form.entregas_escalonadas.length) return false

      const totalPercentual = form.entregas_escalonadas.reduce(
        (sum, e) => sum + (e.percentual || 0),
        0
      )
      if (totalPercentual !== 100) return false

      // Todas as datas preenchidas
      if (form.entregas_escalonadas.some(e => !e.data_prevista)) return false
    }

    return true
  })

  // Entrega escalonada
  const adicionarEntrega = () => {
    if (form.entregas_escalonadas.length >= 5) return

    form.entregas_escalonadas.push({
      ordem: form.entregas_escalonadas.length + 1,
      data_prevista: '',
      percentual: 0,
      quantidade_items: {},
      observacao: ''
    })
  }

  const removerEntrega = (index: number) => {
    form.entregas_escalonadas.splice(index, 1)
    // Reordenar
    form.entregas_escalonadas.forEach((e, i) => e.ordem = i + 1)
  }

  const calcularQuantidadeEtapa = (item: PropostaItem, percentual: number) => {
    return Math.round((item.quantidade * percentual) / 100)
  }

  const totalPercentualEscalonado = computed(() => {
    return form.entregas_escalonadas.reduce((sum, e) => sum + (e.percentual || 0), 0)
  })

  // Submit
  const enviarProposta = async () => {
    if (!formValido.value) return

    // Confirmação
    const confirmar = confirm(
      `Enviar proposta no valor de ${formatCurrency(form.total_final)}?`
    )
    if (!confirmar) return

    isSubmitting.value = true

    try {
      // Preparar payload
      const payload = {
        order_id: form.order_id,
        items: form.items.map(item => ({
          order_item_id: item.order_item_id,
          preco_unitario: item.preco_unitario,
          quantidade: item.quantidade,
          usa_produto_similar: item.usa_produto_similar,
          produto_similar_id: item.produto_similar?.produto_id
        })),
        desconto_percentual: form.desconto_percentual,
        observacoes: form.observacoes,
        aceita_entrega_escalonada: form.aceita_entrega_escalonada,
        entregas_escalonadas: form.aceita_entrega_escalonada
          ? form.entregas_escalonadas
          : undefined
      }

      const { data, error } = await fornecedorApi.criarProposta(payload)

      if (error) throw error

      toast.success('Proposta enviada com sucesso!')

      // Redirecionar
      setTimeout(() => {
        router.push(`/orders/${licitacaoId}`)
      }, 1500)

    } catch (error) {
      console.error('Erro ao enviar proposta:', error)
      toast.error('Erro ao enviar proposta. Tente novamente.')
    } finally {
      isSubmitting.value = false
    }
  }

  return {
    form,
    produtosCotados,
    formValido,
    isSubmitting,
    calcularTotais,
    adicionarEntrega,
    removerEntrega,
    calcularQuantidadeEtapa,
    totalPercentualEscalonado,
    enviarProposta
  }
}

📦 Componente: Modal Produto Similar

<!-- components/propostas/ModalProdutoSimilar.vue -->
<template>
  <Teleport to="body">
    <Transition name="modal">
      <div v-if="show" class="fixed inset-0 z-50 flex items-center justify-center p-4">
        <!-- Backdrop -->
        <div class="absolute inset-0 bg-black bg-opacity-50" @click="fechar"></div>

        <!-- Modal -->
        <div class="relative bg-white rounded-lg shadow-xl max-w-2xl w-full max-h-[90vh] overflow-y-auto">
          <!-- Header -->
          <div class="flex items-center justify-between p-6 border-b border-gray-200">
            <h3 class="text-xl font-bold text-gray-900">
              Oferecer Produto Similar
            </h3>
            <button @click="fechar" class="text-gray-400 hover:text-gray-600">
              <svg class="w-6 h-6">...</svg>
            </button>
          </div>

          <!-- Body -->
          <div class="p-6 space-y-6">
            <!-- Produto Original -->
            <div class="p-4 bg-gray-50 border border-gray-200 rounded-lg">
              <p class="text-xs font-semibold text-gray-500 uppercase mb-1">
                Produto Solicitado
              </p>
              <p class="text-lg font-bold text-gray-900">{{ item?.produto_nome }}</p>
              <p v-if="item?.marca" class="text-sm text-gray-600">
                Marca: {{ item.marca }}
              </p>
            </div>

            <!-- Busca de Produtos -->
            <div>
              <label class="block text-sm font-medium text-gray-700 mb-2">
                Buscar Produto Similar
              </label>
              <input
                v-model="busca"
                @input="buscarProdutos"
                type="text"
                placeholder="Digite o nome ou marca do produto..."
                class="input w-full"
              />
            </div>

            <!-- Lista de Resultados -->
            <div v-if="resultados.length" class="space-y-2 max-h-60 overflow-y-auto">
              <button
                v-for="produto in resultados"
                :key="produto.id"
                @click="selecionarProduto(produto)"
                class="w-full text-left p-4 border rounded-lg hover:border-blue-500 hover:bg-blue-50 transition-colors"
                :class="{
                  'border-blue-500 bg-blue-50': produtoSelecionado?.id === produto.id
                }"
              >
                <p class="font-semibold text-gray-900">{{ produto.nome }}</p>
                <p class="text-sm text-gray-600">{{ produto.marca }}</p>
                <p class="text-xs text-gray-500 mt-1">{{ produto.categoria }}</p>
              </button>
            </div>

            <div v-else-if="busca && !isSearching" class="text-center py-8 text-gray-500">
              Nenhum produto encontrado
            </div>

            <!-- Motivo -->
            <div v-if="produtoSelecionado">
              <label class="block text-sm font-medium text-gray-700 mb-2">
                Por que este produto é similar? *
              </label>
              <textarea
                v-model="motivo"
                rows="3"
                required
                placeholder="Ex: Mesmo princípio ativo, mesma composição, mesma finalidade..."
                class="input w-full"
              ></textarea>
              <p class="text-xs text-gray-500 mt-1">
                Explique ao produtor por que você está oferecendo este produto no lugar do solicitado
              </p>
            </div>
          </div>

          <!-- Footer -->
          <div class="flex items-center justify-end gap-3 p-6 border-t border-gray-200">
            <button @click="fechar" class="btn-outline">
              Cancelar
            </button>
            <button
              @click="confirmar"
              :disabled="!podeConfirmar"
              class="btn-primary"
              :class="{ 'opacity-50 cursor-not-allowed': !podeConfirmar }"
            >
              Confirmar Produto Similar
            </button>
          </div>
        </div>
      </div>
    </Transition>
  </Teleport>
</template>

<script setup lang="ts">
const props = defineProps<{
  show: boolean
  item: PropostaItem | null
}>()

const emit = defineEmits<{
  'update:show': [value: boolean]
  'confirmar': [produto: any, motivo: string]
}>()

const busca = ref('')
const resultados = ref<any[]>([])
const produtoSelecionado = ref<any>(null)
const motivo = ref('')
const isSearching = ref(false)

const podeConfirmar = computed(() => {
  return produtoSelecionado.value && motivo.value.length >= 20
})

const buscarProdutos = useDebounceFn(async () => {
  if (busca.value.length < 3) {
    resultados.value = []
    return
  }

  isSearching.value = true

  try {
    const { data } = await fornecedorApi.buscarProdutos({
      search: busca.value,
      categoria: props.item?.categoria
    })

    resultados.value = data.produtos || []
  } finally {
    isSearching.value = false
  }
}, 500)

const selecionarProduto = (produto: any) => {
  produtoSelecionado.value = produto
}

const confirmar = () => {
  if (!podeConfirmar.value) return

  emit('confirmar', produtoSelecionado.value, motivo.value)
  fechar()
}

const fechar = () => {
  emit('update:show', false)
  busca.value = ''
  resultados.value = []
  produtoSelecionado.value = null
  motivo.value = ''
}
</script>

<style scoped>
.modal-enter-active,
.modal-leave-active {
  transition: opacity 0.3s ease;
}

.modal-enter-from,
.modal-leave-to {
  opacity: 0;
}
</style>

🔌 Backend: Endpoints Necessários

1. Dashboard com Dados Reais

GET /api/v1/fornecedor/dashboard

2. Buscar Produtos Similares

GET /api/v1/fornecedor/produtos/buscar?search=nome&categoria=fertilizantes

3. Criar Proposta com Funcionalidades Novas

POST /api/v1/fornecedor/propostas

Body:
{
  "order_id": "uuid",
  "items": [
    {
      "order_item_id": 123,
      "preco_unitario": 150.00,
      "quantidade": 100,
      "usa_produto_similar": false,
      "produto_similar_id": null
    }
  ],
  "desconto_percentual": 5,
  "observacoes": "Entrega em até 7 dias úteis",
  "aceita_entrega_escalonada": true,
  "entregas_escalonadas": [
    {
      "ordem": 1,
      "data_prevista": "2025-02-01",
      "percentual": 50,
      "observacao": "Primeira parcela"
    },
    {
      "ordem": 2,
      "data_prevista": "2025-03-01",
      "percentual": 50,
      "observacao": "Segunda parcela"
    }
  ]
}

📅 Plano de Implementação

Sprint 1: Correções Críticas (3 dias)

Dia 1: Anonimato

  • Remover nome do produtor de todas as telas
  • Atualizar endpoints backend (remover dados sensíveis)
  • Mostrar apenas cidade/UF
  • Testar em todas as telas

Dia 2: Dashboard Real + Filtros

  • Implementar endpoint /dashboard no backend
  • Integrar composable com API real
  • Remover filtro "Valor Estimado"
  • Adicionar filtros corretos (Similares, Escalonada)
  • Testar paginação e filtros

Dia 3: Testes e Validação

  • Testar fluxo completo
  • Validar anonimato
  • Validar métricas
  • Deploy em staging

Sprint 2: Funcionalidades Novas (5 dias)

Dia 4-5: Desconto e Observações

  • Adicionar campos no formulário
  • Implementar cálculo automático
  • Validações
  • Backend: atualizar endpoint de proposta

Dia 6-7: Produto Similar

  • Criar modal de busca
  • Endpoint de busca de produtos
  • Integrar com formulário
  • Backend: salvar produto similar

Dia 8: Entrega Escalonada

  • Interface de configuração de etapas
  • Cálculo de quantidades
  • Validação de percentuais
  • Backend: salvar entregas escalonadas

Sprint 3: Polish e Testes (2 dias)

Dia 9: UX e Feedback

  • Mensagens de erro claras
  • Loading states
  • Animações suaves
  • Responsividade mobile

Dia 10: Testes Finais

  • Testes E2E completos
  • Validação com stakeholders
  • Deploy em produção

✅ Checklist de Qualidade

Anonimato

  • Nome do produtor removido de todas as páginas
  • Endereço completo não visível
  • Apenas cidade/UF exibidos
  • Teste com múltiplas licitações

Dashboard

  • Métricas carregam dados reais da API
  • Loading states funcionando
  • Erro tratado adequadamente
  • Números animam ao carregar

Filtros

  • "Valor Estimado" removido
  • "Aceita Similares" funciona
  • "Aceita Escalonada" funciona
  • Filtros combinam corretamente

Proposta

  • Cálculo automático funciona
  • Desconto aplica corretamente
  • Observações salvam
  • Produto similar busca e seleciona
  • Entrega escalonada valida 100%
  • Submit envia todos os dados

📈 Métricas de Sucesso

Técnicas

  • Tempo de carregamento dashboard < 1s
  • Zero erros de console
  • Lighthouse score > 90
  • Mobile-first responsivo

Negócio

  • Tempo médio para enviar proposta < 5 min
  • Taxa de conclusão > 85%
  • Uso de produtos similares > 20%
  • Uso de entrega escalonada > 15%

Próximos Passos:

  1. Validar este plano com a equipe
  2. Criar tasks no Linear
  3. Iniciar Sprint 1
  4. Daily reviews

Documento criado por: Engenharia de SoftwareData: 11 de Janeiro de 2025Versão: 2.0

Copyright © 2026