Negócio

Fluxo de Status de Licitações - AgrSis

Data: 2025-12-18 Versão: 1.0 Status: Proposta Aprovada

Fluxo de Status de Licitações - AgrSis

Data: 2025-12-18 Versão: 1.0 Status: Proposta Aprovada


📋 Índice

  1. Visão Geral
  2. Status Disponíveis
  3. Fluxo de Transições
  4. Regras de Negócio
  5. Rastreabilidade
  6. Notificações
  7. Especificação Técnica
  8. Implementação

🎯 Visão Geral

Sistema de gerenciamento de status de licitações com foco em:

  • Rastreabilidade: Histórico completo de mudanças
  • Segurança: Validações e regras de transição
  • Profissionalismo: Comunicação clara com fornecedores
  • Transparência: Motivos obrigatórios para ações críticas

📊 Status Disponíveis

1. DRAFT (Rascunho)

Descrição: Licitação incompleta, em edição Cor: Cinza (#6B7280) Visibilidade: Apenas produtor Ações Disponíveis:

  • ✅ Visualizar
  • ✅ Editar
  • ✅ Excluir (soft delete)

2. PENDING (Pendente/Completa)

Descrição: Licitação completa, pronta para publicar Quando: Campos obrigatórios OK + mínimo 1 produto + clicou "Salvar" no Step 5 Cor: Azul (#3B82F6) Visibilidade: Apenas produtor Ações Disponíveis:

  • ✅ Visualizar
  • ✅ Editar
  • ✅ Publicar → PUBLISHED
  • ✅ Excluir (soft delete)

3. PUBLISHED (Publicada)

Descrição: Licitação publicada, recebendo propostas Cor: Verde (#10B981) Visibilidade: Produtor + Fornecedores (no raio) Ações Disponíveis:

SE 0 propostas:

  • ✅ Visualizar
  • ✅ Cancelar → CANCELLED (sem impacto nos fornecedores)
  • ❌ NÃO pode editar
  • ❌ NÃO pode excluir

SE ≥ 1 proposta:

  • ✅ Visualizar
  • ✅ Revogar → REVOKED (impacta fornecedores, notificação obrigatória)
  • ✅ Adjudicar → AWARDED (escolher vencedor)
  • ❌ NÃO pode cancelar (usar REVOGAR)
  • ❌ NÃO pode editar
  • ❌ NÃO pode excluir

4. CANCELLED (Cancelada)

Descrição: Cancelada antes de receber propostas Cor: Laranja (#F59E0B) Visibilidade: Produtor + Fornecedores que visualizaram Ações Disponíveis:

  • ✅ Visualizar
  • ✅ Editar → volta para PENDING
  • ✅ Publicar novamente → PUBLISHED
  • ❌ NÃO pode excluir

Comunicação:

  • Notificar apenas fornecedores que visualizaram a licitação
  • Mensagem: "Licitação #2025-001 foi cancelada"

5. REVOKED (Revogada)

Descrição: Revogada após receber propostas (ação de alto impacto) Cor: Vermelho Escuro (#DC2626) Visibilidade: Produtor + Todos fornecedores que enviaram proposta Ações Disponíveis:

  • ✅ Visualizar
  • ❌ NÃO pode editar
  • ❌ NÃO pode reabrir
  • ❌ NÃO pode excluir

Comunicação:

  • OBRIGATÓRIO notificar TODOS que enviaram proposta
  • Mensagem: "Licitação #2025-001 foi revogada. Motivo: motivo. Suas propostas foram invalidadas."
  • Email + WhatsApp

Motivo Obrigatório: Ver Motivos de Cancelamento/Revogação


6. AWARDED (Adjudicada)

Descrição: Vencedor escolhido, aguardando finalização Cor: Roxo (#8B5CF6) Visibilidade: Produtor + Vencedor Ações Disponíveis:

  • ✅ Visualizar
  • ✅ Finalizar → FINISHED (após contrato/entrega)
  • ❌ NÃO pode editar
  • ❌ NÃO pode cancelar
  • ❌ NÃO pode excluir

Comunicação:

  • Notificar vencedor com detalhes do contrato
  • Notificar perdedores informando que não foram selecionados (sem detalhes)

7. FINISHED (Finalizada)

Descrição: Processo completo, contrato assinado/entregue Cor: Verde Escuro (#059669) Visibilidade: Produtor + Vencedor Ações Disponíveis:

  • ✅ Visualizar (apenas leitura)
  • ❌ NÃO pode editar
  • ❌ NÃO pode cancelar
  • ❌ NÃO pode excluir

Estado Final: Imutável, apenas para histórico/relatórios


8. DELETED (Excluída - Soft Delete)

Descrição: Excluída pelo produtor (soft delete) Cor: Cinza Escuro (#374151) Visibilidade: Apenas Admin (no painel administrativo) Ações Disponíveis:

  • ✅ Visualizar (apenas Admin)
  • ❌ Usuário NÃO vê mais em suas listas

Metadados Salvos:

  • deleted_at - Timestamp da exclusão
  • deleted_by - User ID de quem excluiu
  • deletion_reason - Motivo da exclusão

🔄 Fluxo de Transições

┌─────────┐
│  DRAFT  │ (Rascunho incompleto)
└────┬────┘
     │ preencher campos
     ↓
┌─────────┐
│ PENDING │ (Completo, pronto para publicar)
└────┬────┘
     │ publicar
     ↓
┌───────────┐
│ PUBLISHED │ (Recebendo propostas)
└─────┬─────┘
      │
      ├─→ 0 propostas → CANCELLED (pode reabrir)
      │                     ↓
      │                 PENDING (editar)
      │                     ↓
      │                 PUBLISHED (publicar novamente)
      │
      ├─→ ≥1 proposta → REVOKED (NÃO pode reabrir)
      │
      └─→ escolher vencedor
          ↓
      ┌─────────┐
      │ AWARDED │ (Vencedor escolhido)
      └────┬────┘
           │ finalizar
           ↓
      ┌──────────┐
      │ FINISHED │ (Processo completo)
      └──────────┘

QUALQUER status (exceto REVOKED, AWARDED, FINISHED) → DELETED (soft delete)

📏 Regras de Negócio

Matriz de Transições Permitidas

De → ParaDRAFTPENDINGPUBLISHEDCANCELLEDREVOKEDAWARDEDFINISHEDDELETED
DRAFT-
PENDING-
PUBLISHED-✅*✅**
CANCELLED-
REVOKED-
AWARDED-
FINISHED-
DELETED-

* Somente se proposals_count = 0 ** Somente se proposals_count > 0

Validações de Transição

// Exemplo de validação
if ($from === OrderStatus::PUBLISHED && $to === OrderStatus::CANCELLED) {
    if ($order->proposals_count > 0) {
        throw new BusinessRuleException(
            'Não pode cancelar licitação com propostas. Use REVOGAR.'
        );
    }
}

if ($from === OrderStatus::PUBLISHED && $to === OrderStatus::REVOKED) {
    if ($order->proposals_count === 0) {
        throw new BusinessRuleException(
            'Não pode revogar licitação sem propostas. Use CANCELAR.'
        );
    }
    if (empty($reason)) {
        throw new ValidationException(
            'Motivo obrigatório para revogar licitação.'
        );
    }
}

🗂️ Rastreabilidade

Tabela de Histórico de Status

CREATE TABLE order_status_history (
    id BIGSERIAL PRIMARY KEY,
    order_id BIGINT NOT NULL,
    from_status VARCHAR(50),
    to_status VARCHAR(50) NOT NULL,
    changed_by BIGINT NOT NULL, -- user_id
    changed_at TIMESTAMP NOT NULL DEFAULT NOW(),
    reason_type VARCHAR(100), -- enum CancellationReason
    reason_notes TEXT, -- texto livre adicional
    ip_address VARCHAR(45),
    user_agent TEXT,

    FOREIGN KEY (order_id) REFERENCES orders(id) ON DELETE CASCADE,
    FOREIGN KEY (changed_by) REFERENCES users(id)
);

CREATE INDEX idx_osh_order_id ON order_status_history(order_id);
CREATE INDEX idx_osh_changed_at ON order_status_history(changed_at);

Campos Adicionais no Order

// Migration para adicionar timestamps
Schema::table('orders', function (Blueprint $table) {
    $table->timestamp('published_at')->nullable();
    $table->timestamp('cancelled_at')->nullable();
    $table->timestamp('revoked_at')->nullable();
    $table->timestamp('awarded_at')->nullable();
    $table->timestamp('finished_at')->nullable();

    // Soft delete com metadados
    $table->softDeletes(); // deleted_at
    $table->unsignedBigInteger('deleted_by')->nullable();
    $table->string('deletion_reason', 500)->nullable();

    $table->foreign('deleted_by')->references('id')->on('users');
});

Motivos de Cancelamento/Revogação

enum CancellationReason: string
{
    case SPECIFICATION_ERROR = 'erro_especificacao';
    case NEED_CHANGE = 'mudanca_necessidade';
    case INSUFFICIENT_BUDGET = 'orcamento_insuficiente';
    case SUPERIOR_DECISION = 'decisao_superior';
    case INADEQUATE_PROPOSALS = 'propostas_inadequadas';
    case DEADLINE_ISSUE = 'problema_prazo';
    case SUPPLIER_ISSUE = 'problema_fornecedor';
    case OTHER = 'outro';

    public function label(): string
    {
        return match($this) {
            self::SPECIFICATION_ERROR => 'Erro na especificação do produto',
            self::NEED_CHANGE => 'Mudança de necessidade',
            self::INSUFFICIENT_BUDGET => 'Orçamento insuficiente',
            self::SUPERIOR_DECISION => 'Decisão superior',
            self::INADEQUATE_PROPOSALS => 'Todas propostas inadequadas',
            self::DEADLINE_ISSUE => 'Problema com prazo de entrega',
            self::SUPPLIER_ISSUE => 'Problema com fornecedores',
            self::OTHER => 'Outro motivo',
        };
    }
}

📬 Notificações

Quando PUBLICAR (PENDING → PUBLISHED)

Destinatários: Fornecedores no raio de fornecimento

Canais: Email + WhatsApp

Mensagem:

🔔 Nova Licitação Disponível!

Licitação: #2025-001
Descrição: Compra de fertilizantes para plantação
Prazo para Propostas: 25/12/2025 às 18:00

Acesse o portal para enviar sua proposta:
[Link para Portal Fornecedor]

Quando CANCELAR (PUBLISHED → CANCELLED, sem propostas)

Destinatários: Apenas fornecedores que visualizaram a licitação

Canais: Email

Mensagem:

❌ Licitação Cancelada

A licitação #2025-001 foi cancelada pelo produtor.

Você não precisa enviar proposta para esta licitação.

Quando REVOGAR (PUBLISHED → REVOKED, com propostas)

Destinatários: TODOS fornecedores que enviaram proposta

Canais: Email + WhatsApp

Mensagem:

⚠️ Licitação Revogada

A licitação #2025-001 foi REVOGADA.

Motivo: Mudança de necessidade

Suas propostas foram invalidadas e não serão consideradas.

Pedimos desculpas pelo transtorno.

Quando ADJUDICAR (PUBLISHED → AWARDED)

Destinatários: Vencedor + Perdedores

Canal: Email + WhatsApp (vencedor) / Email (perdedores)

Mensagem (Vencedor):

🎉 Parabéns! Sua proposta foi a vencedora!

Licitação: #2025-001
Valor Total: R$ 15.000,00

Próximos passos:
1. Acesse o portal para assinar o contrato digital
2. Aguarde confirmação do produtor
3. Proceda com a entrega conforme combinado

[Link para Contrato]

Mensagem (Perdedores):

📋 Resultado da Licitação #2025-001

Informamos que sua proposta não foi selecionada nesta licitação.

Agradecemos sua participação e esperamos contar com você em futuras oportunidades.

🔧 Especificação Técnica

Backend (Laravel API)

1. Enum OrderStatus

<?php

namespace App\Enums;

enum OrderStatus: string
{
    case DRAFT = 'draft';
    case PENDING = 'pending';
    case PUBLISHED = 'published';
    case CANCELLED = 'cancelled';
    case REVOKED = 'revoked';
    case AWARDED = 'awarded';
    case FINISHED = 'finished';

    public function label(): string
    {
        return match($this) {
            self::DRAFT => 'Rascunho',
            self::PENDING => 'Pendente',
            self::PUBLISHED => 'Publicada',
            self::CANCELLED => 'Cancelada',
            self::REVOKED => 'Revogada',
            self::AWARDED => 'Adjudicada',
            self::FINISHED => 'Finalizada',
        };
    }

    public function color(): string
    {
        return match($this) {
            self::DRAFT => '#6B7280',
            self::PENDING => '#3B82F6',
            self::PUBLISHED => '#10B981',
            self::CANCELLED => '#F59E0B',
            self::REVOKED => '#DC2626',
            self::AWARDED => '#8B5CF6',
            self::FINISHED => '#059669',
        };
    }

    public function canTransitionTo(OrderStatus $target): bool
    {
        return match($this) {
            self::DRAFT => in_array($target, [self::PENDING]),
            self::PENDING => in_array($target, [self::DRAFT, self::PUBLISHED]),
            self::PUBLISHED => in_array($target, [self::CANCELLED, self::REVOKED, self::AWARDED]),
            self::CANCELLED => in_array($target, [self::PENDING, self::PUBLISHED]),
            self::REVOKED => false, // nunca muda
            self::AWARDED => in_array($target, [self::FINISHED]),
            self::FINISHED => false, // nunca muda
        };
    }

    public function canDelete(): bool
    {
        return in_array($this, [self::DRAFT, self::PENDING]);
    }

    public function canEdit(): bool
    {
        return in_array($this, [self::DRAFT, self::PENDING, self::CANCELLED]);
    }

    public function canPublish(): bool
    {
        return in_array($this, [self::PENDING, self::CANCELLED]);
    }

    public function canCancel(): bool
    {
        return $this === self::PUBLISHED;
    }

    public function canRevoke(): bool
    {
        return $this === self::PUBLISHED;
    }
}

2. Model OrderStatusHistory

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use App\Enums\OrderStatus;
use App\Enums\CancellationReason;

class OrderStatusHistory extends Model
{
    const UPDATED_AT = null; // Tabela só tem created_at

    protected $table = 'order_status_history';

    protected $fillable = [
        'order_id',
        'from_status',
        'to_status',
        'changed_by',
        'changed_at',
        'reason_type',
        'reason_notes',
        'ip_address',
        'user_agent',
    ];

    protected $casts = [
        'from_status' => OrderStatus::class,
        'to_status' => OrderStatus::class,
        'reason_type' => CancellationReason::class,
        'changed_at' => 'datetime',
    ];

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

    public function changedBy(): BelongsTo
    {
        return $this->belongsTo(User::class, 'changed_by');
    }
}

3. Service OrderStatusService

<?php

namespace App\Services;

use App\Models\Order;
use App\Models\OrderStatusHistory;
use App\Enums\OrderStatus;
use App\Enums\CancellationReason;
use App\Exceptions\InvalidStatusTransitionException;
use App\Exceptions\BusinessRuleException;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\DB;

class OrderStatusService
{
    /**
     * Muda status da licitação com validações e rastreabilidade
     */
    public function changeStatus(
        Order $order,
        OrderStatus $newStatus,
        ?CancellationReason $reason = null,
        ?string $notes = null
    ): Order {
        DB::beginTransaction();

        try {
            $oldStatus = $order->status;

            // 1. Validar se transição é permitida
            if (!$oldStatus->canTransitionTo($newStatus)) {
                throw new InvalidStatusTransitionException(
                    "Não é possível mudar de {$oldStatus->label()} para {$newStatus->label()}"
                );
            }

            // 2. Validar regras de negócio específicas
            $this->validateBusinessRules($order, $oldStatus, $newStatus, $reason);

            // 3. Salvar no histórico ANTES de mudar
            $this->saveHistory($order, $oldStatus, $newStatus, $reason, $notes);

            // 4. Atualizar status e timestamp
            $updates = ['status' => $newStatus];
            $updates[$this->getTimestampField($newStatus)] = now();

            $order->update($updates);

            // 5. Disparar notificações
            $this->notifyStakeholders($order, $newStatus, $reason, $notes);

            // 6. Log de auditoria
            Log::info("Order status changed", [
                'order_id' => $order->id,
                'order_number' => $order->number,
                'from' => $oldStatus->value,
                'to' => $newStatus->value,
                'by' => auth()->id(),
                'reason' => $reason?->value,
            ]);

            DB::commit();

            return $order->fresh();

        } catch (\Exception $e) {
            DB::rollBack();
            throw $e;
        }
    }

    /**
     * Validações de regras de negócio
     */
    protected function validateBusinessRules(
        Order $order,
        OrderStatus $from,
        OrderStatus $to,
        ?CancellationReason $reason
    ): void {
        // Regra: Cancelar só se não tiver propostas
        if ($from === OrderStatus::PUBLISHED && $to === OrderStatus::CANCELLED) {
            if ($order->proposals_count > 0) {
                throw new BusinessRuleException(
                    'Não pode cancelar licitação com propostas. Use REVOGAR.'
                );
            }
        }

        // Regra: Revogar só se tiver propostas
        if ($from === OrderStatus::PUBLISHED && $to === OrderStatus::REVOKED) {
            if ($order->proposals_count === 0) {
                throw new BusinessRuleException(
                    'Não pode revogar licitação sem propostas. Use CANCELAR.'
                );
            }

            if (empty($reason)) {
                throw new BusinessRuleException(
                    'Motivo obrigatório para revogar licitação.'
                );
            }
        }

        // Regra: Publicar só se completo
        if ($to === OrderStatus::PUBLISHED) {
            if (!$this->isOrderComplete($order)) {
                throw new BusinessRuleException(
                    'Licitação incompleta. Preencha todos os campos obrigatórios.'
                );
            }
        }
    }

    /**
     * Verifica se licitação está completa
     */
    protected function isOrderComplete(Order $order): bool
    {
        return !empty($order->description)
            && !empty($order->deadline_date)
            && !empty($order->supply_radius)
            && $order->orderItems()->count() > 0;
    }

    /**
     * Salva histórico de mudança de status
     */
    protected function saveHistory(
        Order $order,
        OrderStatus $from,
        OrderStatus $to,
        ?CancellationReason $reason,
        ?string $notes
    ): void {
        OrderStatusHistory::create([
            'order_id' => $order->id,
            'from_status' => $from,
            'to_status' => $to,
            'changed_by' => auth()->id(),
            'changed_at' => now(),
            'reason_type' => $reason,
            'reason_notes' => $notes,
            'ip_address' => request()->ip(),
            'user_agent' => request()->userAgent(),
        ]);
    }

    /**
     * Retorna campo de timestamp apropriado para o status
     */
    protected function getTimestampField(OrderStatus $status): ?string
    {
        return match($status) {
            OrderStatus::PUBLISHED => 'published_at',
            OrderStatus::CANCELLED => 'cancelled_at',
            OrderStatus::REVOKED => 'revoked_at',
            OrderStatus::AWARDED => 'awarded_at',
            OrderStatus::FINISHED => 'finished_at',
            default => null,
        };
    }

    /**
     * Notifica stakeholders sobre mudança de status
     */
    protected function notifyStakeholders(
        Order $order,
        OrderStatus $newStatus,
        ?CancellationReason $reason,
        ?string $notes
    ): void {
        // TODO: Implementar notificações (Email/WhatsApp)
        // Ver seção "Notificações" deste documento
    }
}

4. Controller Methods

// OrderController.php

/**
 * Excluir licitação (soft delete com metadados)
 */
public function destroy(Order $order, Request $request): JsonResponse
{
    $request->validate([
        'reason' => 'required|string|max:500',
    ]);

    try {
        // Verificar se pode excluir
        if (!$order->status->canDelete()) {
            return response()->json([
                'message' => "Não é possível excluir licitação com status {$order->status->label()}"
            ], 422);
        }

        // Salvar histórico
        OrderStatusHistory::create([
            'order_id' => $order->id,
            'from_status' => $order->status,
            'to_status' => null,
            'changed_by' => auth()->id(),
            'changed_at' => now(),
            'reason_type' => null,
            'reason_notes' => 'DELETED: ' . $request->reason,
            'ip_address' => $request->ip(),
            'user_agent' => $request->userAgent(),
        ]);

        // Soft delete com metadados
        $order->update([
            'deleted_by' => auth()->id(),
            'deletion_reason' => $request->reason,
        ]);
        $order->delete();

        return response()->json([
            'message' => 'Licitação excluída com sucesso'
        ]);

    } catch (\Exception $e) {
        return response()->json([
            'message' => 'Erro ao excluir licitação',
            'error' => $e->getMessage()
        ], 500);
    }
}

/**
 * Cancelar licitação (PUBLISHED → CANCELLED, sem propostas)
 */
public function cancel(Order $order, Request $request): JsonResponse
{
    $request->validate([
        'reason_type' => 'required|string',
        'reason_notes' => 'nullable|string|max:1000',
    ]);

    try {
        $statusService = new OrderStatusService();

        $order = $statusService->changeStatus(
            $order,
            OrderStatus::CANCELLED,
            CancellationReason::from($request->reason_type),
            $request->reason_notes
        );

        return response()->json([
            'message' => 'Licitação cancelada com sucesso',
            'data' => new OrderResource($order)
        ]);

    } catch (\Exception $e) {
        return response()->json([
            'message' => $e->getMessage()
        ], 422);
    }
}

/**
 * Revogar licitação (PUBLISHED → REVOKED, com propostas)
 */
public function revoke(Order $order, Request $request): JsonResponse
{
    $request->validate([
        'reason_type' => 'required|string',
        'reason_notes' => 'nullable|string|max:1000',
    ]);

    try {
        $statusService = new OrderStatusService();

        $order = $statusService->changeStatus(
            $order,
            OrderStatus::REVOKED,
            CancellationReason::from($request->reason_type),
            $request->reason_notes
        );

        return response()->json([
            'message' => 'Licitação revogada com sucesso',
            'data' => new OrderResource($order)
        ]);

    } catch (\Exception $e) {
        return response()->json([
            'message' => $e->getMessage()
        ], 422);
    }
}

Frontend (Nuxt 3)

1. Types

// types/order.ts

export enum OrderStatus {
  DRAFT = 'draft',
  PENDING = 'pending',
  PUBLISHED = 'published',
  CANCELLED = 'cancelled',
  REVOKED = 'revoked',
  AWARDED = 'awarded',
  FINISHED = 'finished',
}

export enum CancellationReason {
  SPECIFICATION_ERROR = 'erro_especificacao',
  NEED_CHANGE = 'mudanca_necessidade',
  INSUFFICIENT_BUDGET = 'orcamento_insuficiente',
  SUPERIOR_DECISION = 'decisao_superior',
  INADEQUATE_PROPOSALS = 'propostas_inadequadas',
  DEADLINE_ISSUE = 'problema_prazo',
  SUPPLIER_ISSUE = 'problema_fornecedor',
  OTHER = 'outro',
}

export interface Order {
  id: number
  uuid: string
  number: string
  status: OrderStatus
  proposals_count: number
  published_at?: string
  cancelled_at?: string
  revoked_at?: string
  awarded_at?: string
  finished_at?: string
  // ... outros campos
}

export interface OrderStatusHistoryItem {
  id: number
  from_status: OrderStatus | null
  to_status: OrderStatus
  changed_by_name: string
  changed_at: string
  reason_type?: CancellationReason
  reason_notes?: string
}

2. Modal de Exclusão (com motivo)

<!-- Modal de Exclusão -->
<div v-if="showDeleteModal" class="modal">
  <div class="modal-content">
    <h3>Excluir Licitação?</h3>
    <p>Esta ação não pode ser desfeita.</p>

    <div class="form-group">
      <label>Motivo da exclusão:</label>
      <textarea
        v-model="deletionReason"
        required
        placeholder="Explique por que está excluindo esta licitação..."
      ></textarea>
    </div>

    <div class="actions">
      <button @click="cancelarExclusao">Cancelar</button>
      <button @click="confirmarExclusao" :disabled="!deletionReason">
        Confirmar Exclusão
      </button>
    </div>
  </div>
</div>

3. Modal de Cancelamento (com seleção de motivo)

<!-- Modal de Cancelamento -->
<div v-if="showCancelModal" class="modal">
  <div class="modal-content">
    <h3>Cancelar Licitação?</h3>
    <p>Esta licitação ainda não recebeu propostas.</p>

    <div class="form-group">
      <label>Motivo do cancelamento:</label>
      <select v-model="cancellationReasonType" required>
        <option value="">Selecione um motivo</option>
        <option value="erro_especificacao">Erro na especificação</option>
        <option value="mudanca_necessidade">Mudança de necessidade</option>
        <option value="orcamento_insuficiente">Orçamento insuficiente</option>
        <option value="decisao_superior">Decisão superior</option>
        <option value="outro">Outro</option>
      </select>
    </div>

    <div v-if="cancellationReasonType" class="form-group">
      <label>Observações adicionais (opcional):</label>
      <textarea v-model="cancellationNotes" placeholder="Detalhes..."></textarea>
    </div>

    <div class="actions">
      <button @click="cancelarCancelamento">Voltar</button>
      <button @click="confirmarCancelamento" :disabled="!cancellationReasonType">
        Confirmar Cancelamento
      </button>
    </div>
  </div>
</div>

4. Modal de Revogação (com alerta de impacto)

<!-- Modal de Revogação -->
<div v-if="showRevokeModal" class="modal">
  <div class="modal-content warning">
    <h3>⚠️ Revogar Licitação?</h3>

    <div class="alert alert-danger">
      <strong>ATENÇÃO:</strong> Esta licitação já recebeu
      <strong>{{ selectedOrder.proposals_count }} proposta(s)</strong>.

      Ao revogar:
      - Todos os fornecedores serão notificados
      - As propostas serão invalidadas
      - Não será possível reabrir esta licitação
    </div>

    <div class="form-group">
      <label>Motivo da revogação (obrigatório):</label>
      <select v-model="revocationReasonType" required>
        <option value="">Selecione um motivo</option>
        <option value="erro_especificacao">Erro na especificação</option>
        <option value="mudanca_necessidade">Mudança de necessidade</option>
        <option value="propostas_inadequadas">Propostas inadequadas</option>
        <option value="decisao_superior">Decisão superior</option>
        <option value="outro">Outro</option>
      </select>
    </div>

    <div class="form-group">
      <label>Explicação detalhada (obrigatório):</label>
      <textarea
        v-model="revocationNotes"
        required
        placeholder="Explique detalhadamente o motivo da revogação. Esta mensagem será enviada aos fornecedores."
      ></textarea>
    </div>

    <div class="actions">
      <button @click="cancelarRevogacao">Cancelar</button>
      <button
        @click="confirmarRevogacao"
        :disabled="!revocationReasonType || !revocationNotes"
        class="btn-danger"
      >
        Confirmar Revogação
      </button>
    </div>
  </div>
</div>

📝 Implementação

Ordem de Implementação

Fase 1: Backend - Estrutura Base

  1. ✅ Criar enum CancellationReason
  2. ✅ Atualizar enum OrderStatus com novos status
  3. ✅ Criar migration para order_status_history
  4. ✅ Criar migration para adicionar campos timestamp no orders
  5. ✅ Criar model OrderStatusHistory
  6. ✅ Atualizar model Order com campos de timestamp

Fase 2: Backend - Lógica de Negócio

  1. ✅ Criar OrderStatusService com validações
  2. ✅ Criar testes unitários para OrderStatus transitions
  3. ✅ Criar testes unitários para OrderStatusService
  4. ✅ Atualizar OrderController::destroy para soft delete
  5. ✅ Criar OrderController::cancel
  6. ✅ Criar OrderController::revoke
  7. ✅ Criar testes para endpoints de cancel/revoke/delete

Fase 3: Frontend - Types e Estrutura

  1. ✅ Atualizar types/order.ts com novos status
  2. ✅ Criar types/cancellation-reason.ts
  3. ✅ Atualizar composable useOrdersApi com novos métodos

Fase 4: Frontend - UI/UX

  1. ✅ Criar modal de exclusão com campo de motivo
  2. ✅ Criar modal de cancelamento com seleção de motivo
  3. ✅ Criar modal de revogação com alerta de impacto
  4. ✅ Atualizar lista de licitações (diferenciar ações por status)
  5. ✅ Criar componente de timeline de histórico de status
  6. ✅ Atualizar badges de status com novas cores

Fase 5: Notificações

  1. ⏳ Implementar notificação de publicação
  2. ⏳ Implementar notificação de cancelamento
  3. ⏳ Implementar notificação de revogação
  4. ⏳ Implementar notificação de adjudicação

Fase 6: Admin

  1. ⏳ Dashboard admin com licitações deletadas
  2. ⏳ Filtros por todos os status (incluindo DELETED)
  3. ⏳ Visualização de histórico completo
  4. ⏳ Relatórios de licitações (por status, motivos, etc)

📊 Métricas e Relatórios

Métricas Importantes:

  1. Taxa de Cancelamento
    • % de licitações canceladas vs total publicadas
    • Principais motivos de cancelamento
  2. Taxa de Revogação
    • % de licitações revogadas vs total publicadas
    • Impacto médio (nº de fornecedores afetados)
  3. Tempo Médio por Status
    • DRAFT → PENDING: X dias
    • PENDING → PUBLISHED: Y dias
    • PUBLISHED → AWARDED: Z dias
  4. Taxa de Sucesso
    • % de licitações que chegam a FINISHED
    • % de licitações que ficam em REVOKED/CANCELLED

🔍 Auditoria e Compliance

Logs Obrigatórios:

Todos os eventos críticos devem ser logados:

Log::channel('audit')->info('Order status changed', [
    'order_id' => $order->id,
    'order_number' => $order->number,
    'from_status' => $oldStatus->value,
    'to_status' => $newStatus->value,
    'changed_by' => auth()->id(),
    'changed_by_name' => auth()->user()->name,
    'ip_address' => request()->ip(),
    'user_agent' => request()->userAgent(),
    'reason_type' => $reason?->value,
    'reason_notes' => $notes,
    'timestamp' => now()->toIso8601String(),
]);

Relatórios de Auditoria:

Admin pode gerar relatórios de:

  • Todas mudanças de status por período
  • Licitações revogadas (com motivos)
  • Licitações deletadas (com motivos)
  • Ações por usuário

✅ Checklist de Conclusão

  • Todos os status implementados
  • Validações de transição funcionando
  • Histórico salvando corretamente
  • Timestamps sendo preenchidos
  • Soft delete com metadados
  • Testes unitários (100% cobertura)
  • Modais de confirmação implementados
  • Notificações funcionando
  • Dashboard admin completo
  • Documentação atualizada

Documento mantido por: AgrSis Dev Team Última atualização: 2025-12-18

Copyright © 2026