667 lines
19 KiB
Svelte
667 lines
19 KiB
Svelte
<script lang="ts">
|
||
import { api } from '@sgse-app/backend/convex/_generated/api';
|
||
import type { Id } from '@sgse-app/backend/convex/_generated/dataModel';
|
||
import { useConvexClient, useQuery } from 'convex-svelte';
|
||
import {
|
||
AlertTriangle,
|
||
CheckCircle,
|
||
Clock,
|
||
Edit,
|
||
Plus,
|
||
Save,
|
||
Send,
|
||
Trash2,
|
||
X,
|
||
XCircle
|
||
} from 'lucide-svelte';
|
||
import { page } from '$app/stores';
|
||
import { maskCurrencyBRL } from '$lib/utils/masks';
|
||
|
||
const pedidoId = $page.params.id as Id<'pedidos'>;
|
||
const client = useConvexClient();
|
||
|
||
// Reactive queries
|
||
const pedidoQuery = useQuery(api.pedidos.get, { id: pedidoId });
|
||
const itemsQuery = useQuery(api.pedidos.getItems, { pedidoId });
|
||
const historyQuery = useQuery(api.pedidos.getHistory, { pedidoId });
|
||
const objetosQuery = useQuery(api.objetos.list, {});
|
||
const acoesQuery = useQuery(api.acoes.list, {});
|
||
|
||
// Derived state
|
||
let pedido = $derived(pedidoQuery.data);
|
||
let items = $derived(itemsQuery.data || []);
|
||
let history = $derived(historyQuery.data || []);
|
||
let objetos = $derived(objetosQuery.data || []);
|
||
let acoes = $derived(acoesQuery.data || []);
|
||
|
||
// Group items by user
|
||
let groupedItems = $derived.by(() => {
|
||
const groups: Record<string, { name: string; items: typeof items }> = {};
|
||
for (const item of items) {
|
||
const userId = item.adicionadoPor;
|
||
if (!groups[userId]) {
|
||
groups[userId] = {
|
||
name: item.adicionadoPorNome,
|
||
items: []
|
||
};
|
||
}
|
||
groups[userId].items.push(item);
|
||
}
|
||
return Object.values(groups);
|
||
});
|
||
|
||
let loading = $derived(
|
||
pedidoQuery.isLoading ||
|
||
itemsQuery.isLoading ||
|
||
historyQuery.isLoading ||
|
||
objetosQuery.isLoading ||
|
||
acoesQuery.isLoading
|
||
);
|
||
|
||
let error = $derived(
|
||
pedidoQuery.error?.message ||
|
||
itemsQuery.error?.message ||
|
||
historyQuery.error?.message ||
|
||
objetosQuery.error?.message ||
|
||
acoesQuery.error?.message ||
|
||
null
|
||
);
|
||
|
||
// Add Item State
|
||
let showAddItem = $state(false);
|
||
let newItem = $state({
|
||
objetoId: '' as string,
|
||
valorEstimado: '',
|
||
quantidade: 1,
|
||
modalidade: 'consumo' as 'dispensa' | 'inexgibilidade' | 'adesao' | 'consumo',
|
||
acaoId: '' as string
|
||
});
|
||
let addingItem = $state(false);
|
||
|
||
// Edit SEI State
|
||
let editingSei = $state(false);
|
||
let seiValue = $state('');
|
||
let updatingSei = $state(false);
|
||
|
||
async function handleAddItem() {
|
||
if (!newItem.objetoId || !newItem.valorEstimado) return;
|
||
addingItem = true;
|
||
try {
|
||
await client.mutation(api.pedidos.addItem, {
|
||
pedidoId,
|
||
objetoId: newItem.objetoId as Id<'objetos'>,
|
||
valorEstimado: newItem.valorEstimado,
|
||
quantidade: newItem.quantidade,
|
||
modalidade: newItem.modalidade,
|
||
acaoId: newItem.acaoId ? (newItem.acaoId as Id<'acoes'>) : undefined
|
||
});
|
||
newItem = {
|
||
objetoId: '',
|
||
valorEstimado: '',
|
||
quantidade: 1,
|
||
modalidade: 'consumo',
|
||
acaoId: ''
|
||
};
|
||
showAddItem = false;
|
||
} catch (e) {
|
||
alert('Erro ao adicionar item: ' + (e as Error).message);
|
||
} finally {
|
||
addingItem = false;
|
||
}
|
||
}
|
||
|
||
async function handleUpdateQuantity(itemId: Id<'objetoItems'>, novaQuantidade: number) {
|
||
if (novaQuantidade < 1) {
|
||
alert('Quantidade deve ser pelo menos 1.');
|
||
return;
|
||
}
|
||
try {
|
||
await client.mutation(api.pedidos.updateItemQuantity, {
|
||
itemId,
|
||
novaQuantidade
|
||
});
|
||
} catch (e) {
|
||
alert('Erro ao atualizar quantidade: ' + (e as Error).message);
|
||
}
|
||
}
|
||
|
||
async function handleRemoveItem(itemId: Id<'objetoItems'>) {
|
||
if (!confirm('Remover este item?')) return;
|
||
try {
|
||
await client.mutation(api.pedidos.removeItem, { itemId });
|
||
} catch (e) {
|
||
alert('Erro ao remover item: ' + (e as Error).message);
|
||
}
|
||
}
|
||
|
||
async function updateStatus(
|
||
novoStatus:
|
||
| 'cancelado'
|
||
| 'concluido'
|
||
| 'em_rascunho'
|
||
| 'aguardando_aceite'
|
||
| 'em_analise'
|
||
| 'precisa_ajustes'
|
||
) {
|
||
if (!confirm(`Confirmar alteração de status para: ${novoStatus}?`)) return;
|
||
try {
|
||
await client.mutation(api.pedidos.updateStatus, {
|
||
pedidoId,
|
||
novoStatus
|
||
});
|
||
} catch (e) {
|
||
alert('Erro ao atualizar status: ' + (e as Error).message);
|
||
}
|
||
}
|
||
|
||
function getObjetoName(id: string) {
|
||
return objetos.find((o) => o._id === id)?.nome || 'Objeto desconhecido';
|
||
}
|
||
|
||
function getAcaoName(id: string | undefined) {
|
||
if (!id) return '-';
|
||
return acoes.find((a) => a._id === id)?.nome || '-';
|
||
}
|
||
|
||
function handleObjetoChange(id: string) {
|
||
newItem.objetoId = id;
|
||
const objeto = objetos.find((o) => o._id === id);
|
||
if (objeto) {
|
||
newItem.valorEstimado = maskCurrencyBRL(objeto.valorEstimado || '');
|
||
} else {
|
||
newItem.valorEstimado = '';
|
||
}
|
||
}
|
||
|
||
function parseMoneyToNumber(value: string): number {
|
||
const cleanValue = value.replace(/[^0-9,]/g, '').replace(',', '.');
|
||
return parseFloat(cleanValue) || 0;
|
||
}
|
||
|
||
function calculateItemTotal(valorEstimado: string, quantidade: number): number {
|
||
const unitValue = parseMoneyToNumber(valorEstimado);
|
||
return unitValue * quantidade;
|
||
}
|
||
|
||
let totalGeral = $derived(
|
||
items.reduce((sum, item) => sum + calculateItemTotal(item.valorEstimado, item.quantidade), 0)
|
||
);
|
||
|
||
function formatStatus(status: string) {
|
||
switch (status) {
|
||
case 'em_rascunho':
|
||
return 'Rascunho';
|
||
case 'aguardando_aceite':
|
||
return 'Aguardando Aceite';
|
||
case 'em_analise':
|
||
return 'Em Análise';
|
||
case 'precisa_ajustes':
|
||
return 'Precisa de Ajustes';
|
||
case 'concluido':
|
||
return 'Concluído';
|
||
case 'cancelado':
|
||
return 'Cancelado';
|
||
default:
|
||
return status;
|
||
}
|
||
}
|
||
|
||
async function handleUpdateSei() {
|
||
if (!seiValue.trim()) {
|
||
alert('O número SEI não pode estar vazio.');
|
||
return;
|
||
}
|
||
updatingSei = true;
|
||
try {
|
||
await client.mutation(api.pedidos.updateSeiNumber, {
|
||
pedidoId,
|
||
numeroSei: seiValue.trim()
|
||
});
|
||
editingSei = false;
|
||
} catch (e) {
|
||
alert('Erro ao atualizar número SEI: ' + (e as Error).message);
|
||
} finally {
|
||
updatingSei = false;
|
||
}
|
||
}
|
||
|
||
function startEditingSei() {
|
||
seiValue = pedido?.numeroSei || '';
|
||
editingSei = true;
|
||
}
|
||
|
||
function cancelEditingSei() {
|
||
editingSei = false;
|
||
seiValue = '';
|
||
}
|
||
|
||
function getStatusColor(status: string) {
|
||
switch (status) {
|
||
case 'em_rascunho':
|
||
return 'bg-gray-100 text-gray-800';
|
||
case 'aguardando_aceite':
|
||
return 'bg-yellow-100 text-yellow-800';
|
||
case 'em_analise':
|
||
return 'bg-blue-100 text-blue-800';
|
||
case 'precisa_ajustes':
|
||
return 'bg-orange-100 text-orange-800';
|
||
case 'concluido':
|
||
return 'bg-green-100 text-green-800';
|
||
case 'cancelado':
|
||
return 'bg-red-100 text-red-800';
|
||
default:
|
||
return 'bg-gray-100 text-gray-800';
|
||
}
|
||
}
|
||
|
||
function getHistoryIcon(acao: string) {
|
||
switch (acao) {
|
||
case 'criacao_pedido':
|
||
return '📝';
|
||
case 'adicao_item':
|
||
return '➕';
|
||
case 'remocao_item':
|
||
return '🗑️';
|
||
case 'alteracao_quantidade':
|
||
return '🔢';
|
||
case 'alteracao_status':
|
||
return '🔄';
|
||
case 'atualizacao_sei':
|
||
return '📋';
|
||
default:
|
||
return '•';
|
||
}
|
||
}
|
||
|
||
function formatHistoryEntry(entry: {
|
||
acao: string;
|
||
detalhes: string | undefined;
|
||
usuarioNome: string;
|
||
}): string {
|
||
try {
|
||
const detalhes = entry.detalhes ? JSON.parse(entry.detalhes) : {};
|
||
|
||
switch (entry.acao) {
|
||
case 'criacao_pedido':
|
||
return `${entry.usuarioNome} criou o pedido ${detalhes.numeroSei || ''}`;
|
||
|
||
case 'adicao_item': {
|
||
const objeto = objetos.find((o) => o._id === detalhes.objetoId);
|
||
const nomeObjeto = objeto?.nome || 'Objeto desconhecido';
|
||
const quantidade = detalhes.quantidade || 1;
|
||
return `${entry.usuarioNome} adicionou ${quantidade}x ${nomeObjeto} (${detalhes.valor})`;
|
||
}
|
||
|
||
case 'remocao_item': {
|
||
const objeto = objetos.find((o) => o._id === detalhes.objetoId);
|
||
const nomeObjeto = objeto?.nome || 'Objeto desconhecido';
|
||
return `${entry.usuarioNome} removeu ${nomeObjeto}`;
|
||
}
|
||
|
||
case 'alteracao_quantidade': {
|
||
const objeto = objetos.find((o) => o._id === detalhes.objetoId);
|
||
const nomeObjeto = objeto?.nome || 'Objeto desconhecido';
|
||
return `${entry.usuarioNome} alterou a quantidade de ${nomeObjeto} de ${detalhes.quantidadeAnterior} para ${detalhes.novaQuantidade}`;
|
||
}
|
||
|
||
case 'alteracao_status':
|
||
return `${entry.usuarioNome} alterou o status para "${formatStatus(detalhes.novoStatus)}"`;
|
||
case 'atualizacao_sei':
|
||
return `${entry.usuarioNome} atualizou o número SEI para "${detalhes.numeroSei}"`;
|
||
|
||
default:
|
||
return `${entry.usuarioNome} realizou: ${entry.acao}`;
|
||
}
|
||
} catch {
|
||
return `${entry.usuarioNome} - ${entry.acao}`;
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<div class="container mx-auto p-6">
|
||
{#if loading}
|
||
<p>Carregando...</p>
|
||
{:else if error}
|
||
<p class="text-red-600">{error}</p>
|
||
{:else if pedido}
|
||
<div class="mb-6 flex items-start justify-between">
|
||
<div>
|
||
<h1 class="flex items-center gap-3 text-2xl font-bold">
|
||
{#if editingSei}
|
||
<div class="flex items-center gap-2">
|
||
<input
|
||
type="text"
|
||
bind:value={seiValue}
|
||
class="rounded border px-2 py-1 text-sm"
|
||
placeholder="Número SEI"
|
||
disabled={updatingSei}
|
||
/>
|
||
<button
|
||
onclick={handleUpdateSei}
|
||
disabled={updatingSei}
|
||
class="rounded bg-green-600 p-1 text-white hover:bg-green-700 disabled:opacity-50"
|
||
title="Salvar"
|
||
>
|
||
<Save size={16} />
|
||
</button>
|
||
<button
|
||
onclick={cancelEditingSei}
|
||
disabled={updatingSei}
|
||
class="rounded bg-gray-400 p-1 text-white hover:bg-gray-500 disabled:opacity-50"
|
||
title="Cancelar"
|
||
>
|
||
<X size={16} />
|
||
</button>
|
||
</div>
|
||
{:else}
|
||
<div class="flex items-center gap-2">
|
||
<span>Pedido {pedido.numeroSei || 'sem número SEI'}</span>
|
||
{#if pedido.status === 'em_rascunho' || pedido.status === 'precisa_ajustes'}
|
||
<button
|
||
onclick={startEditingSei}
|
||
class="rounded bg-blue-100 p-1 text-blue-600 hover:bg-blue-200"
|
||
title="Editar número SEI"
|
||
>
|
||
<Edit size={16} />
|
||
</button>
|
||
{/if}
|
||
</div>
|
||
{/if}
|
||
<span class="rounded-full px-3 py-1 text-sm font-medium {getStatusColor(pedido.status)}">
|
||
{formatStatus(pedido.status)}
|
||
</span>
|
||
</h1>
|
||
{#if !pedido.numeroSei}
|
||
<p class="mt-1 text-sm text-amber-600">
|
||
⚠️ Este pedido não possui número SEI. Adicione um número SEI quando disponível.
|
||
</p>
|
||
{/if}
|
||
</div>
|
||
|
||
<div class="flex gap-2">
|
||
{#if pedido.status === 'em_rascunho' || pedido.status === 'precisa_ajustes'}
|
||
<button
|
||
onclick={() => updateStatus('aguardando_aceite')}
|
||
class="flex items-center gap-2 rounded bg-blue-600 px-4 py-2 text-white hover:bg-blue-700"
|
||
>
|
||
<Send size={18} /> Enviar para Aceite
|
||
</button>
|
||
{/if}
|
||
|
||
{#if pedido.status === 'aguardando_aceite'}
|
||
<button
|
||
onclick={() => updateStatus('em_analise')}
|
||
class="flex items-center gap-2 rounded bg-indigo-600 px-4 py-2 text-white hover:bg-indigo-700"
|
||
>
|
||
<Clock size={18} /> Iniciar Análise
|
||
</button>
|
||
{/if}
|
||
|
||
{#if pedido.status === 'em_analise'}
|
||
<button
|
||
onclick={() => updateStatus('concluido')}
|
||
class="flex items-center gap-2 rounded bg-green-600 px-4 py-2 text-white hover:bg-green-700"
|
||
>
|
||
<CheckCircle size={18} /> Concluir
|
||
</button>
|
||
<button
|
||
onclick={() => updateStatus('precisa_ajustes')}
|
||
class="flex items-center gap-2 rounded bg-orange-500 px-4 py-2 text-white hover:bg-orange-600"
|
||
>
|
||
<AlertTriangle size={18} /> Solicitar Ajustes
|
||
</button>
|
||
{/if}
|
||
|
||
{#if pedido.status !== 'cancelado' && pedido.status !== 'concluido'}
|
||
<button
|
||
onclick={() => updateStatus('cancelado')}
|
||
class="flex items-center gap-2 rounded bg-red-100 px-4 py-2 text-red-700 hover:bg-red-200"
|
||
>
|
||
<XCircle size={18} /> Cancelar
|
||
</button>
|
||
{/if}
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Items Section -->
|
||
<div class="mb-6 overflow-hidden rounded-lg bg-white shadow-md">
|
||
<div class="flex items-center justify-between border-b border-gray-200 px-6 py-4">
|
||
<h2 class="text-lg font-semibold">Itens do Pedido</h2>
|
||
{#if pedido.status === 'em_rascunho' || pedido.status === 'precisa_ajustes'}
|
||
<button
|
||
onclick={() => (showAddItem = true)}
|
||
class="flex items-center gap-1 text-sm font-medium text-blue-600 hover:text-blue-800"
|
||
>
|
||
<Plus size={16} /> Adicionar Item
|
||
</button>
|
||
{/if}
|
||
</div>
|
||
|
||
{#if showAddItem}
|
||
<div class="border-b border-gray-200 bg-gray-50 px-6 py-4">
|
||
<div class="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||
<div class="col-span-1 md:col-span-2 lg:col-span-1">
|
||
<label for="objeto-select" class="mb-1 block text-xs font-medium text-gray-500"
|
||
>Objeto</label
|
||
>
|
||
<select
|
||
id="objeto-select"
|
||
bind:value={newItem.objetoId}
|
||
onchange={(e) => handleObjetoChange(e.currentTarget.value)}
|
||
class="w-full rounded-md border-gray-300 text-sm shadow-sm"
|
||
>
|
||
<option value="">Selecione...</option>
|
||
{#each objetos as o (o._id)}
|
||
<option value={o._id}>{o.nome} ({o.unidade})</option>
|
||
{/each}
|
||
</select>
|
||
</div>
|
||
<div>
|
||
<label for="quantidade-input" class="mb-1 block text-xs font-medium text-gray-500"
|
||
>Quantidade</label
|
||
>
|
||
<input
|
||
id="quantidade-input"
|
||
type="number"
|
||
min="1"
|
||
bind:value={newItem.quantidade}
|
||
class="w-full rounded-md border-gray-300 text-sm shadow-sm"
|
||
placeholder="1"
|
||
/>
|
||
</div>
|
||
<div>
|
||
<label for="valor-input" class="mb-1 block text-xs font-medium text-gray-500"
|
||
>Valor Estimado</label
|
||
>
|
||
<input
|
||
id="valor-input"
|
||
type="text"
|
||
bind:value={newItem.valorEstimado}
|
||
oninput={(e) => (newItem.valorEstimado = maskCurrencyBRL(e.currentTarget.value))}
|
||
class="w-full rounded-md border-gray-300 text-sm shadow-sm"
|
||
placeholder="R$ 0,00"
|
||
/>
|
||
</div>
|
||
<div>
|
||
<label for="modalidade-select" class="mb-1 block text-xs font-medium text-gray-500"
|
||
>Modalidade</label
|
||
>
|
||
<select
|
||
id="modalidade-select"
|
||
bind:value={newItem.modalidade}
|
||
class="w-full rounded-md border-gray-300 text-sm shadow-sm"
|
||
>
|
||
<option value="consumo">Consumo</option>
|
||
<option value="dispensa">Dispensa</option>
|
||
<option value="inexgibilidade">Inexigibilidade</option>
|
||
<option value="adesao">Adesão</option>
|
||
</select>
|
||
</div>
|
||
<div>
|
||
<label for="acao-select" class="mb-1 block text-xs font-medium text-gray-500"
|
||
>Ação (Opcional)</label
|
||
>
|
||
<select
|
||
id="acao-select"
|
||
bind:value={newItem.acaoId}
|
||
class="w-full rounded-md border-gray-300 text-sm shadow-sm"
|
||
>
|
||
<option value="">Selecione...</option>
|
||
{#each acoes as a (a._id)}
|
||
<option value={a._id}>{a.nome}</option>
|
||
{/each}
|
||
</select>
|
||
</div>
|
||
</div>
|
||
<div class="mt-4 flex justify-end gap-2">
|
||
<button
|
||
onclick={() => (showAddItem = false)}
|
||
class="rounded bg-gray-200 px-3 py-2 text-sm text-gray-700 hover:bg-gray-300"
|
||
>
|
||
Cancelar
|
||
</button>
|
||
<button
|
||
onclick={handleAddItem}
|
||
disabled={addingItem}
|
||
class="rounded bg-blue-600 px-3 py-2 text-sm text-white hover:bg-blue-700"
|
||
>
|
||
Adicionar
|
||
</button>
|
||
</div>
|
||
</div>
|
||
{/if}
|
||
|
||
<div class="flex flex-col">
|
||
{#each groupedItems as group (group.name)}
|
||
<div class="border-b border-gray-200 bg-gray-100 px-6 py-2 font-medium text-gray-700">
|
||
Adicionado por: {group.name}
|
||
</div>
|
||
<table class="min-w-full divide-y divide-gray-200">
|
||
<thead class="bg-gray-50">
|
||
<tr>
|
||
<th
|
||
class="px-6 py-3 text-left text-xs font-medium tracking-wider text-gray-500 uppercase"
|
||
>Objeto</th
|
||
>
|
||
<th
|
||
class="px-6 py-3 text-left text-xs font-medium tracking-wider text-gray-500 uppercase"
|
||
>Qtd</th
|
||
>
|
||
<th
|
||
class="px-6 py-3 text-left text-xs font-medium tracking-wider text-gray-500 uppercase"
|
||
>Valor Est.</th
|
||
>
|
||
<th
|
||
class="px-6 py-3 text-left text-xs font-medium tracking-wider text-gray-500 uppercase"
|
||
>Modalidade</th
|
||
>
|
||
<th
|
||
class="px-6 py-3 text-left text-xs font-medium tracking-wider text-gray-500 uppercase"
|
||
>Ação</th
|
||
>
|
||
<th
|
||
class="px-6 py-3 text-right text-xs font-medium tracking-wider text-gray-500 uppercase"
|
||
>Total</th
|
||
>
|
||
<th
|
||
class="px-6 py-3 text-right text-xs font-medium tracking-wider text-gray-500 uppercase"
|
||
>Ações</th
|
||
>
|
||
</tr>
|
||
</thead>
|
||
<tbody class="divide-y divide-gray-200 bg-white">
|
||
{#each group.items as item (item._id)}
|
||
<tr>
|
||
<td class="px-6 py-4 whitespace-nowrap">{getObjetoName(item.objetoId)}</td>
|
||
<td class="px-6 py-4 whitespace-nowrap">
|
||
{#if pedido.status === 'em_rascunho' || pedido.status === 'precisa_ajustes'}
|
||
<input
|
||
type="number"
|
||
min="1"
|
||
value={item.quantidade}
|
||
onchange={(e) =>
|
||
handleUpdateQuantity(item._id, parseInt(e.currentTarget.value) || 1)}
|
||
class="w-20 rounded border px-2 py-1 text-sm"
|
||
/>
|
||
{:else}
|
||
{item.quantidade}
|
||
{/if}
|
||
</td>
|
||
<td class="px-6 py-4 whitespace-nowrap">
|
||
{maskCurrencyBRL(item.valorEstimado) || 'R$ 0,00'}
|
||
</td>
|
||
<td class="px-6 py-4 text-sm whitespace-nowrap text-gray-600">
|
||
{item.modalidade}
|
||
</td>
|
||
<td class="px-6 py-4 text-sm whitespace-nowrap text-gray-600">
|
||
{getAcaoName(item.acaoId)}
|
||
</td>
|
||
<td class="px-6 py-4 text-right font-medium whitespace-nowrap">
|
||
R$ {calculateItemTotal(item.valorEstimado, item.quantidade)
|
||
.toFixed(2)
|
||
.replace('.', ',')}
|
||
</td>
|
||
<td class="px-6 py-4 text-right text-sm font-medium whitespace-nowrap">
|
||
{#if pedido.status === 'em_rascunho' || pedido.status === 'precisa_ajustes'}
|
||
<button
|
||
onclick={() => handleRemoveItem(item._id)}
|
||
class="text-red-600 hover:text-red-900"
|
||
>
|
||
<Trash2 size={16} />
|
||
</button>
|
||
{/if}
|
||
</td>
|
||
</tr>
|
||
{/each}
|
||
</tbody>
|
||
</table>
|
||
{/each}
|
||
|
||
{#if items.length === 0}
|
||
<div class="px-6 py-4 text-center text-gray-500">Nenhum item adicionado.</div>
|
||
{:else}
|
||
<div class="flex justify-end bg-gray-50 px-6 py-4">
|
||
<div class="text-base font-bold text-gray-900">
|
||
Total Geral: R$ {totalGeral.toFixed(2).replace('.', ',')}
|
||
</div>
|
||
</div>
|
||
{/if}
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Histórico -->
|
||
<div class="rounded-lg bg-white p-6 shadow">
|
||
<h2 class="mb-4 text-lg font-semibold text-gray-900">Histórico</h2>
|
||
<div class="space-y-3">
|
||
{#if history.length === 0}
|
||
<p class="text-sm text-gray-500">Nenhum histórico disponível.</p>
|
||
{:else}
|
||
{#each history as entry (entry._id)}
|
||
<div
|
||
class="flex items-start gap-3 rounded-lg border border-gray-200 p-3 hover:bg-gray-50"
|
||
>
|
||
<div class="flex-shrink-0 text-2xl">
|
||
{getHistoryIcon(entry.acao)}
|
||
</div>
|
||
<div class="min-w-0 flex-1">
|
||
<p class="text-sm text-gray-900">
|
||
{formatHistoryEntry(entry)}
|
||
</p>
|
||
<p class="mt-1 text-xs text-gray-500">
|
||
{new Date(entry.data).toLocaleString('pt-BR', {
|
||
day: '2-digit',
|
||
month: '2-digit',
|
||
year: 'numeric',
|
||
hour: '2-digit',
|
||
minute: '2-digit'
|
||
})}
|
||
</p>
|
||
</div>
|
||
</div>
|
||
{/each}
|
||
{/if}
|
||
</div>
|
||
</div>
|
||
{/if}
|
||
</div>
|