Fluxo de Status de Licitações - AgrSis
Fluxo de Status de Licitações - AgrSis
Data: 2025-12-18 Versão: 1.0 Status: Proposta Aprovada
📋 Índice
- Visão Geral
- Status Disponíveis
- Fluxo de Transições
- Regras de Negócio
- Rastreabilidade
- Notificações
- Especificação Técnica
- 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ãodeleted_by- User ID de quem excluiudeletion_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 → Para | DRAFT | PENDING | PUBLISHED | CANCELLED | REVOKED | AWARDED | FINISHED | DELETED |
|---|---|---|---|---|---|---|---|---|
| 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
- ✅ Criar enum
CancellationReason - ✅ Atualizar enum
OrderStatuscom novos status - ✅ Criar migration para
order_status_history - ✅ Criar migration para adicionar campos timestamp no
orders - ✅ Criar model
OrderStatusHistory - ✅ Atualizar model
Ordercom campos de timestamp
Fase 2: Backend - Lógica de Negócio
- ✅ Criar
OrderStatusServicecom validações - ✅ Criar testes unitários para
OrderStatustransitions - ✅ Criar testes unitários para
OrderStatusService - ✅ Atualizar
OrderController::destroypara soft delete - ✅ Criar
OrderController::cancel - ✅ Criar
OrderController::revoke - ✅ Criar testes para endpoints de cancel/revoke/delete
Fase 3: Frontend - Types e Estrutura
- ✅ Atualizar
types/order.tscom novos status - ✅ Criar
types/cancellation-reason.ts - ✅ Atualizar composable
useOrdersApicom novos métodos
Fase 4: Frontend - UI/UX
- ✅ Criar modal de exclusão com campo de motivo
- ✅ Criar modal de cancelamento com seleção de motivo
- ✅ Criar modal de revogação com alerta de impacto
- ✅ Atualizar lista de licitações (diferenciar ações por status)
- ✅ Criar componente de timeline de histórico de status
- ✅ Atualizar badges de status com novas cores
Fase 5: Notificações
- ⏳ Implementar notificação de publicação
- ⏳ Implementar notificação de cancelamento
- ⏳ Implementar notificação de revogação
- ⏳ Implementar notificação de adjudicação
Fase 6: Admin
- ⏳ Dashboard admin com licitações deletadas
- ⏳ Filtros por todos os status (incluindo DELETED)
- ⏳ Visualização de histórico completo
- ⏳ Relatórios de licitações (por status, motivos, etc)
📊 Métricas e Relatórios
Métricas Importantes:
- Taxa de Cancelamento
- % de licitações canceladas vs total publicadas
- Principais motivos de cancelamento
- Taxa de Revogação
- % de licitações revogadas vs total publicadas
- Impacto médio (nº de fornecedores afetados)
- Tempo Médio por Status
- DRAFT → PENDING: X dias
- PENDING → PUBLISHED: Y dias
- PUBLISHED → AWARDED: Z dias
- 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