feat: enhance pedidos functionality by adding new submenu options for creating and planning orders, improving user navigation and access control in the sidebar; also implement URL-based prefill for adding items, ensuring a smoother user experience when creating pedidos
This commit is contained in:
@@ -20,9 +20,10 @@
|
||||
X,
|
||||
XCircle
|
||||
} from 'lucide-svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import { afterNavigate, goto } from '$app/navigation';
|
||||
import { resolve } from '$app/paths';
|
||||
import { page } from '$app/state';
|
||||
import { onMount } from 'svelte';
|
||||
import { maskCurrencyBRL } from '$lib/utils/masks';
|
||||
import { formatarDataBR } from '$lib/utils/datas';
|
||||
|
||||
@@ -167,7 +168,6 @@
|
||||
itemsQuery.isLoading ||
|
||||
historyQuery.isLoading ||
|
||||
objetosQuery.isLoading ||
|
||||
objetosQuery.isLoading ||
|
||||
acoesQuery.isLoading ||
|
||||
permissionsQuery.isLoading ||
|
||||
requestsQuery.isLoading ||
|
||||
@@ -394,6 +394,7 @@
|
||||
|
||||
// Add Item State
|
||||
let showAddItem = $state(false);
|
||||
let hasAppliedAddItemPrefill = $state(false);
|
||||
let newItem = $state({
|
||||
objetoId: '' as string,
|
||||
valorEstimado: '',
|
||||
@@ -404,7 +405,46 @@
|
||||
});
|
||||
let addingItem = $state(false);
|
||||
|
||||
let hasAppliedPrefill = $state(false);
|
||||
function applyAddItemPrefillFromUrl() {
|
||||
if (hasAppliedAddItemPrefill) return;
|
||||
|
||||
const obj = page.url.searchParams.get('obj');
|
||||
if (!obj) return;
|
||||
|
||||
const qtdRaw = page.url.searchParams.get('qtd');
|
||||
const qtd = qtdRaw ? Number.parseInt(qtdRaw, 10) : 1;
|
||||
|
||||
const mod = page.url.searchParams.get('mod') ?? '';
|
||||
const acao = page.url.searchParams.get('acao') ?? '';
|
||||
const ata = page.url.searchParams.get('ata') ?? '';
|
||||
|
||||
showAddItem = true;
|
||||
newItem.objetoId = obj;
|
||||
newItem.quantidade = Number.isFinite(qtd) && qtd > 0 ? qtd : 1;
|
||||
newItem.modalidade = coerceModalidade(mod);
|
||||
newItem.acaoId = acao;
|
||||
newItem.ataId = ata;
|
||||
|
||||
const objeto = objetos.find((o: Doc<'objetos'>) => o._id === obj);
|
||||
newItem.valorEstimado = maskCurrencyBRL(objeto?.valorEstimado || '');
|
||||
|
||||
void loadAtasForObjeto(obj);
|
||||
|
||||
hasAppliedAddItemPrefill = true;
|
||||
void goto(resolve(`/pedidos/${pedidoId}`), {
|
||||
replaceState: true,
|
||||
noScroll: true,
|
||||
keepFocus: true
|
||||
});
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
applyAddItemPrefillFromUrl();
|
||||
});
|
||||
|
||||
afterNavigate(() => {
|
||||
applyAddItemPrefillFromUrl();
|
||||
});
|
||||
|
||||
// Edit SEI State
|
||||
let editingSei = $state(false);
|
||||
@@ -470,47 +510,6 @@
|
||||
selectedObjeto = null;
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
if (hasAppliedPrefill) return;
|
||||
if (objetosQuery.isLoading || acoesQuery.isLoading) return;
|
||||
|
||||
const url = page.url;
|
||||
const obj = url.searchParams.get('obj');
|
||||
const qtdStr = url.searchParams.get('qtd');
|
||||
const mod = url.searchParams.get('mod') as Modalidade | null;
|
||||
const acao = url.searchParams.get('acao');
|
||||
const ata = url.searchParams.get('ata');
|
||||
|
||||
if (!obj) return;
|
||||
|
||||
const objeto = objetos.find((o) => o._id === obj);
|
||||
if (!objeto) return;
|
||||
|
||||
let quantidade = parseInt(qtdStr || '1', 10);
|
||||
if (!Number.isFinite(quantidade) || quantidade <= 0) {
|
||||
quantidade = 1;
|
||||
}
|
||||
|
||||
const modalidade: Modalidade =
|
||||
mod === 'dispensa' || mod === 'inexgibilidade' || mod === 'adesao' || mod === 'consumo'
|
||||
? mod
|
||||
: 'consumo';
|
||||
|
||||
showAddItem = true;
|
||||
newItem = {
|
||||
objetoId: obj,
|
||||
valorEstimado: maskCurrencyBRL(objeto.valorEstimado || ''),
|
||||
quantidade,
|
||||
modalidade,
|
||||
acaoId: acao || '',
|
||||
ataId: ata || ''
|
||||
};
|
||||
|
||||
void loadAtasForObjeto(obj);
|
||||
|
||||
hasAppliedPrefill = true;
|
||||
});
|
||||
|
||||
async function handleAddItem() {
|
||||
if (!pedido || !newItem.objetoId || !newItem.valorEstimado) return;
|
||||
|
||||
@@ -519,17 +518,19 @@
|
||||
if (items.length > 0) {
|
||||
const referenceItem = items[0];
|
||||
|
||||
const referenceModalidade = referenceItem.modalidade as Modalidade;
|
||||
const referenceModalidade = (referenceItem.modalidade as Modalidade | undefined) ?? undefined;
|
||||
const referenceAtaId = (('ataId' in referenceItem ? referenceItem.ataId : undefined) ??
|
||||
null) as string | null;
|
||||
|
||||
const newAtaId = newItem.ataId || null;
|
||||
|
||||
const sameModalidade = newItem.modalidade === referenceModalidade;
|
||||
const sameModalidade = !referenceModalidade || newItem.modalidade === referenceModalidade;
|
||||
const sameAta = referenceAtaId === newAtaId;
|
||||
|
||||
if (!sameModalidade || !sameAta) {
|
||||
const refModalidadeLabel = formatModalidade(referenceModalidade);
|
||||
const refModalidadeLabel = referenceModalidade
|
||||
? formatModalidade(referenceModalidade)
|
||||
: 'Não definida';
|
||||
const refAtaLabel =
|
||||
referenceAtaId === null ? 'sem Ata vinculada' : 'com uma Ata específica';
|
||||
|
||||
@@ -740,7 +741,7 @@
|
||||
|
||||
return {
|
||||
valorEstimado: maskCurrencyBRL(item.valorEstimado || ''),
|
||||
modalidade: item.modalidade,
|
||||
modalidade: coerceModalidade((item.modalidade as string | undefined) ?? 'consumo'),
|
||||
acaoId: item.acaoId ?? '',
|
||||
ataId: item.ataId ?? ''
|
||||
};
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
export const load = async ({ parent }) => {
|
||||
await parent();
|
||||
};
|
||||
@@ -0,0 +1,305 @@
|
||||
<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 { Plus, Eye, X } from 'lucide-svelte';
|
||||
import { resolve } from '$app/paths';
|
||||
import { goto } from '$app/navigation';
|
||||
import { toast } from 'svelte-sonner';
|
||||
|
||||
const client = useConvexClient();
|
||||
|
||||
const planejamentosQuery = useQuery(api.planejamentos.list, {});
|
||||
const funcionariosQuery = useQuery(api.funcionarios.getAll, {});
|
||||
const acoesQuery = useQuery(api.acoes.list, {});
|
||||
|
||||
let planejamentos = $derived(planejamentosQuery.data || []);
|
||||
|
||||
function formatStatus(status: string) {
|
||||
switch (status) {
|
||||
case 'rascunho':
|
||||
return 'Rascunho';
|
||||
case 'gerado':
|
||||
return 'Gerado';
|
||||
case 'cancelado':
|
||||
return 'Cancelado';
|
||||
default:
|
||||
return status;
|
||||
}
|
||||
}
|
||||
|
||||
function getStatusColor(status: string) {
|
||||
switch (status) {
|
||||
case 'rascunho':
|
||||
return 'bg-gray-100 text-gray-800';
|
||||
case 'gerado':
|
||||
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 formatDateYMD(ymd: string) {
|
||||
// ymd: yyyy-MM-dd
|
||||
const [y, m, d] = ymd.split('-');
|
||||
if (!y || !m || !d) return ymd;
|
||||
return `${d}/${m}/${y}`;
|
||||
}
|
||||
|
||||
// Create modal
|
||||
let showCreate = $state(false);
|
||||
let creating = $state(false);
|
||||
let form = $state({
|
||||
titulo: '',
|
||||
descricao: '',
|
||||
data: '',
|
||||
responsavelId: '' as string,
|
||||
acaoId: '' as string
|
||||
});
|
||||
|
||||
function openCreate() {
|
||||
form = { titulo: '', descricao: '', data: '', responsavelId: '', acaoId: '' };
|
||||
showCreate = true;
|
||||
}
|
||||
|
||||
function closeCreate() {
|
||||
showCreate = false;
|
||||
}
|
||||
|
||||
async function handleCreate() {
|
||||
if (!form.titulo.trim()) return toast.error('Informe um título.');
|
||||
if (!form.descricao.trim()) return toast.error('Informe uma descrição.');
|
||||
if (!form.data.trim()) return toast.error('Informe uma data.');
|
||||
if (!form.responsavelId) return toast.error('Selecione um responsável.');
|
||||
|
||||
creating = true;
|
||||
try {
|
||||
const id = await client.mutation(api.planejamentos.create, {
|
||||
titulo: form.titulo,
|
||||
descricao: form.descricao,
|
||||
data: form.data,
|
||||
responsavelId: form.responsavelId as Id<'funcionarios'>,
|
||||
acaoId: form.acaoId ? (form.acaoId as Id<'acoes'>) : undefined
|
||||
});
|
||||
toast.success('Planejamento criado.');
|
||||
showCreate = false;
|
||||
await goto(resolve(`/pedidos/planejamento/${id}`));
|
||||
} catch (e) {
|
||||
toast.error((e as Error).message);
|
||||
} finally {
|
||||
creating = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="container mx-auto p-6">
|
||||
<div class="mb-6 flex items-center justify-between">
|
||||
<h1 class="text-2xl font-bold">Planejamento de Pedidos</h1>
|
||||
<button
|
||||
type="button"
|
||||
class="flex items-center gap-2 rounded-md bg-blue-600 px-4 py-2 text-white hover:bg-blue-700"
|
||||
onclick={openCreate}
|
||||
>
|
||||
<Plus size={20} />
|
||||
Novo planejamento
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{#if planejamentosQuery.isLoading}
|
||||
<div class="flex flex-col gap-4">
|
||||
{#each Array(3)}
|
||||
<div class="skeleton h-16 w-full rounded-lg"></div>
|
||||
{/each}
|
||||
</div>
|
||||
{:else if planejamentosQuery.error}
|
||||
<div class="alert alert-error">
|
||||
<span>{planejamentosQuery.error.message}</span>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="overflow-hidden rounded-lg bg-white shadow-md">
|
||||
<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"
|
||||
>
|
||||
Título
|
||||
</th>
|
||||
<th
|
||||
class="px-6 py-3 text-left text-xs font-medium tracking-wider text-gray-500 uppercase"
|
||||
>
|
||||
Data
|
||||
</th>
|
||||
<th
|
||||
class="px-6 py-3 text-left text-xs font-medium tracking-wider text-gray-500 uppercase"
|
||||
>
|
||||
Responsável
|
||||
</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-left text-xs font-medium tracking-wider text-gray-500 uppercase"
|
||||
>
|
||||
Status
|
||||
</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 planejamentos as p (p._id)}
|
||||
<tr class="hover:bg-gray-50">
|
||||
<td class="px-6 py-4 font-medium whitespace-nowrap">{p.titulo}</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-gray-600">{formatDateYMD(p.data)}</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-gray-600">{p.responsavelNome}</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-gray-600">{p.acaoNome || '-'}</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<span
|
||||
class="inline-flex rounded-full px-2 py-1 text-xs font-semibold {getStatusColor(
|
||||
p.status
|
||||
)}"
|
||||
>
|
||||
{formatStatus(p.status)}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-4 text-right text-sm font-medium whitespace-nowrap">
|
||||
<a
|
||||
href={resolve(`/pedidos/planejamento/${p._id}`)}
|
||||
class="inline-flex items-center gap-1 text-indigo-600 hover:text-indigo-900"
|
||||
>
|
||||
<Eye size={18} />
|
||||
Abrir
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
{#if planejamentos.length === 0}
|
||||
<tr>
|
||||
<td colspan="6" class="px-6 py-4 text-center text-gray-500"
|
||||
>Nenhum planejamento encontrado.</td
|
||||
>
|
||||
</tr>
|
||||
{/if}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if showCreate}
|
||||
<div
|
||||
class="fixed inset-0 z-50 flex h-full w-full items-center justify-center overflow-y-auto bg-black/40 p-4"
|
||||
>
|
||||
<div class="relative w-full max-w-2xl rounded-xl bg-white p-6 shadow-2xl">
|
||||
<button
|
||||
type="button"
|
||||
onclick={closeCreate}
|
||||
class="absolute top-4 right-4 rounded-lg p-1 text-gray-400 transition hover:bg-gray-100 hover:text-gray-600"
|
||||
aria-label="Fechar"
|
||||
disabled={creating}
|
||||
>
|
||||
<X size={24} />
|
||||
</button>
|
||||
|
||||
<h2 class="mb-4 text-xl font-bold text-gray-900">Novo planejamento</h2>
|
||||
|
||||
<div class="grid gap-4 md:grid-cols-2">
|
||||
<div class="md:col-span-2">
|
||||
<label class="mb-1 block text-sm font-medium text-gray-700" for="titulo">Título</label>
|
||||
<input
|
||||
id="titulo"
|
||||
type="text"
|
||||
class="w-full rounded-lg border border-gray-300 px-4 py-2.5 text-gray-700 shadow-sm focus:border-blue-500 focus:ring-2 focus:ring-blue-200 focus:outline-none"
|
||||
bind:value={form.titulo}
|
||||
disabled={creating}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="md:col-span-2">
|
||||
<label class="mb-1 block text-sm font-medium text-gray-700" for="descricao"
|
||||
>Descrição</label
|
||||
>
|
||||
<textarea
|
||||
id="descricao"
|
||||
class="w-full rounded-lg border border-gray-300 px-4 py-2.5 text-gray-700 shadow-sm focus:border-blue-500 focus:ring-2 focus:ring-blue-200 focus:outline-none"
|
||||
rows="4"
|
||||
bind:value={form.descricao}
|
||||
disabled={creating}
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="mb-1 block text-sm font-medium text-gray-700" for="data">Data</label>
|
||||
<input
|
||||
id="data"
|
||||
type="date"
|
||||
class="w-full rounded-lg border border-gray-300 px-4 py-2.5 text-gray-700 shadow-sm focus:border-blue-500 focus:ring-2 focus:ring-blue-200 focus:outline-none"
|
||||
bind:value={form.data}
|
||||
disabled={creating}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="mb-1 block text-sm font-medium text-gray-700" for="responsavel"
|
||||
>Responsável</label
|
||||
>
|
||||
<select
|
||||
id="responsavel"
|
||||
class="w-full rounded-lg border border-gray-300 bg-white px-4 py-2.5 text-gray-700 shadow-sm focus:border-blue-500 focus:ring-2 focus:ring-blue-200 focus:outline-none"
|
||||
bind:value={form.responsavelId}
|
||||
disabled={creating || funcionariosQuery.isLoading}
|
||||
>
|
||||
<option value="">Selecione...</option>
|
||||
{#each funcionariosQuery.data || [] as f (f._id)}
|
||||
<option value={f._id}>{f.nome}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="md:col-span-2">
|
||||
<label class="mb-1 block text-sm font-medium text-gray-700" for="acao"
|
||||
>Ação (opcional)</label
|
||||
>
|
||||
<select
|
||||
id="acao"
|
||||
class="w-full rounded-lg border border-gray-300 bg-white px-4 py-2.5 text-gray-700 shadow-sm focus:border-blue-500 focus:ring-2 focus:ring-blue-200 focus:outline-none"
|
||||
bind:value={form.acaoId}
|
||||
disabled={creating || acoesQuery.isLoading}
|
||||
>
|
||||
<option value="">Nenhuma</option>
|
||||
{#each acoesQuery.data || [] as a (a._id)}
|
||||
<option value={a._id}>{a.nome}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-6 flex justify-end gap-2">
|
||||
<button
|
||||
type="button"
|
||||
class="rounded-lg bg-gray-200 px-5 py-2.5 font-semibold text-gray-800 transition hover:bg-gray-300"
|
||||
onclick={closeCreate}
|
||||
disabled={creating}
|
||||
>
|
||||
Cancelar
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="rounded-lg bg-blue-600 px-5 py-2.5 font-semibold text-white shadow-md transition hover:bg-blue-700 disabled:opacity-50"
|
||||
onclick={handleCreate}
|
||||
disabled={creating}
|
||||
>
|
||||
{creating ? 'Criando...' : 'Criar'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -0,0 +1,3 @@
|
||||
export const load = async ({ parent }) => {
|
||||
await parent();
|
||||
};
|
||||
@@ -0,0 +1,875 @@
|
||||
<script lang="ts">
|
||||
import { api } from '@sgse-app/backend/convex/_generated/api';
|
||||
import type { Doc, Id } from '@sgse-app/backend/convex/_generated/dataModel';
|
||||
import { useConvexClient, useQuery } from 'convex-svelte';
|
||||
import { page } from '$app/state';
|
||||
import { resolve } from '$app/paths';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { Plus, Trash2, X, Save, Edit } from 'lucide-svelte';
|
||||
|
||||
const client = useConvexClient();
|
||||
const planejamentoId = $derived(page.params.id as Id<'planejamentosPedidos'>);
|
||||
|
||||
const planejamentoQuery = $derived.by(() =>
|
||||
useQuery(api.planejamentos.get, { id: planejamentoId })
|
||||
);
|
||||
const itemsQuery = $derived.by(() => useQuery(api.planejamentos.listItems, { planejamentoId }));
|
||||
const pedidosQuery = $derived.by(() =>
|
||||
useQuery(api.planejamentos.listPedidos, { planejamentoId })
|
||||
);
|
||||
|
||||
const funcionariosQuery = useQuery(api.funcionarios.getAll, {});
|
||||
const acoesQuery = useQuery(api.acoes.list, {});
|
||||
|
||||
let planejamento = $derived(planejamentoQuery.data);
|
||||
let items = $derived(itemsQuery.data || []);
|
||||
let pedidosLinks = $derived(pedidosQuery.data || []);
|
||||
|
||||
const isRascunho = $derived(planejamento?.status === 'rascunho');
|
||||
|
||||
function formatStatus(status: string) {
|
||||
switch (status) {
|
||||
case 'rascunho':
|
||||
return 'Rascunho';
|
||||
case 'gerado':
|
||||
return 'Gerado';
|
||||
case 'cancelado':
|
||||
return 'Cancelado';
|
||||
default:
|
||||
return status;
|
||||
}
|
||||
}
|
||||
|
||||
function getStatusColor(status: string) {
|
||||
switch (status) {
|
||||
case 'rascunho':
|
||||
return 'bg-gray-100 text-gray-800';
|
||||
case 'gerado':
|
||||
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 formatPedidoStatus(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;
|
||||
}
|
||||
}
|
||||
|
||||
function getPedidoStatusColor(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';
|
||||
}
|
||||
}
|
||||
|
||||
// --- Header editing ---
|
||||
let editingHeader = $state(false);
|
||||
let headerForm = $state({
|
||||
titulo: '',
|
||||
descricao: '',
|
||||
data: '',
|
||||
responsavelId: '' as string,
|
||||
acaoId: '' as string
|
||||
});
|
||||
let savingHeader = $state(false);
|
||||
|
||||
function syncHeaderFormFromPlanejamento() {
|
||||
if (!planejamento) return;
|
||||
headerForm = {
|
||||
titulo: planejamento.titulo,
|
||||
descricao: planejamento.descricao,
|
||||
data: planejamento.data,
|
||||
responsavelId: planejamento.responsavelId as unknown as string,
|
||||
acaoId: planejamento.acaoId ? (planejamento.acaoId as unknown as string) : ''
|
||||
};
|
||||
}
|
||||
|
||||
function startEditHeader() {
|
||||
if (!isRascunho) return;
|
||||
syncHeaderFormFromPlanejamento();
|
||||
editingHeader = true;
|
||||
}
|
||||
|
||||
function cancelEditHeader() {
|
||||
editingHeader = false;
|
||||
syncHeaderFormFromPlanejamento();
|
||||
}
|
||||
|
||||
async function saveHeader() {
|
||||
if (!planejamento) return;
|
||||
if (!headerForm.titulo.trim()) return toast.error('Informe um título.');
|
||||
if (!headerForm.descricao.trim()) return toast.error('Informe uma descrição.');
|
||||
if (!headerForm.data.trim()) return toast.error('Informe uma data.');
|
||||
if (!headerForm.responsavelId) return toast.error('Selecione um responsável.');
|
||||
|
||||
savingHeader = true;
|
||||
try {
|
||||
await client.mutation(api.planejamentos.update, {
|
||||
id: planejamentoId,
|
||||
titulo: headerForm.titulo,
|
||||
descricao: headerForm.descricao,
|
||||
data: headerForm.data,
|
||||
responsavelId: headerForm.responsavelId as Id<'funcionarios'>,
|
||||
acaoId: headerForm.acaoId ? (headerForm.acaoId as Id<'acoes'>) : null
|
||||
});
|
||||
toast.success('Planejamento atualizado.');
|
||||
editingHeader = false;
|
||||
} catch (e) {
|
||||
toast.error((e as Error).message);
|
||||
} finally {
|
||||
savingHeader = false;
|
||||
}
|
||||
}
|
||||
|
||||
// --- Add item (search + modal) ---
|
||||
let searchQuery = $state('');
|
||||
const searchResultsQuery = useQuery(api.objetos.search, () => ({ query: searchQuery }));
|
||||
let searchResults = $derived(searchResultsQuery.data || []);
|
||||
|
||||
type SelectedObjeto = Doc<'objetos'>;
|
||||
let showAddItemModal = $state(false);
|
||||
let addItemConfig = $state<{
|
||||
objeto: SelectedObjeto | null;
|
||||
quantidade: number;
|
||||
valorEstimado: string;
|
||||
numeroDfd: string;
|
||||
}>({
|
||||
objeto: null,
|
||||
quantidade: 1,
|
||||
valorEstimado: '',
|
||||
numeroDfd: ''
|
||||
});
|
||||
let addingItem = $state(false);
|
||||
|
||||
function openAddItemModal(objeto: SelectedObjeto) {
|
||||
addItemConfig = {
|
||||
objeto,
|
||||
quantidade: 1,
|
||||
valorEstimado: objeto.valorEstimado,
|
||||
numeroDfd: ''
|
||||
};
|
||||
showAddItemModal = true;
|
||||
searchQuery = '';
|
||||
}
|
||||
|
||||
function closeAddItemModal() {
|
||||
showAddItemModal = false;
|
||||
addItemConfig.objeto = null;
|
||||
}
|
||||
|
||||
async function confirmAddItem() {
|
||||
if (!addItemConfig.objeto) return;
|
||||
if (!isRascunho)
|
||||
return toast.error('Não é possível adicionar itens em um planejamento não rascunho.');
|
||||
if (!Number.isFinite(addItemConfig.quantidade) || addItemConfig.quantidade <= 0) {
|
||||
return toast.error('Quantidade inválida.');
|
||||
}
|
||||
if (!addItemConfig.valorEstimado.trim()) return toast.error('Informe o valor estimado.');
|
||||
|
||||
addingItem = true;
|
||||
try {
|
||||
await client.mutation(api.planejamentos.addItem, {
|
||||
planejamentoId,
|
||||
objetoId: addItemConfig.objeto._id,
|
||||
quantidade: addItemConfig.quantidade,
|
||||
valorEstimado: addItemConfig.valorEstimado,
|
||||
numeroDfd: addItemConfig.numeroDfd.trim() || undefined
|
||||
});
|
||||
toast.success('Item adicionado.');
|
||||
closeAddItemModal();
|
||||
} catch (e) {
|
||||
toast.error((e as Error).message);
|
||||
} finally {
|
||||
addingItem = false;
|
||||
}
|
||||
}
|
||||
|
||||
// --- Inline item edits ---
|
||||
async function updateItemField(
|
||||
itemId: Id<'planejamentoItens'>,
|
||||
patch: { numeroDfd?: string | null; quantidade?: number; valorEstimado?: string }
|
||||
) {
|
||||
if (!isRascunho) return;
|
||||
try {
|
||||
await client.mutation(api.planejamentos.updateItem, { itemId, ...patch });
|
||||
} catch (e) {
|
||||
toast.error((e as Error).message);
|
||||
}
|
||||
}
|
||||
|
||||
async function removeItem(itemId: Id<'planejamentoItens'>) {
|
||||
if (!isRascunho) return;
|
||||
try {
|
||||
await client.mutation(api.planejamentos.removeItem, { itemId });
|
||||
toast.success('Item removido.');
|
||||
} catch (e) {
|
||||
toast.error((e as Error).message);
|
||||
}
|
||||
}
|
||||
|
||||
// --- Grouping ---
|
||||
let pedidosByDfd = $derived.by(() => {
|
||||
const map: Record<string, (typeof pedidosLinks)[number]> = {};
|
||||
for (const p of pedidosLinks) map[p.numeroDfd] = p;
|
||||
return map;
|
||||
});
|
||||
|
||||
let grouped = $derived.by(() => {
|
||||
const groups: Record<string, typeof items> = {};
|
||||
const semDfd: typeof items = [];
|
||||
|
||||
for (const it of items) {
|
||||
const dfd = (it.numeroDfd || '').trim();
|
||||
if (!dfd) {
|
||||
semDfd.push(it);
|
||||
continue;
|
||||
}
|
||||
if (!groups[dfd]) groups[dfd] = [];
|
||||
groups[dfd].push(it);
|
||||
}
|
||||
|
||||
const out: Array<{ key: string; label: string; items: typeof items; isSemDfd: boolean }> = [];
|
||||
if (semDfd.length > 0)
|
||||
out.push({ key: '__sem_dfd__', label: 'Sem DFD', items: semDfd, isSemDfd: true });
|
||||
|
||||
const keys = Object.keys(groups).sort((a, b) => a.localeCompare(b));
|
||||
for (const k of keys) {
|
||||
out.push({ key: k, label: `DFD ${k}`, items: groups[k], isSemDfd: false });
|
||||
}
|
||||
|
||||
return out;
|
||||
});
|
||||
|
||||
let itensSemDfdCount = $derived(grouped.find((g) => g.isSemDfd)?.items.length ?? 0);
|
||||
let dfdsParaGerar = $derived.by(() => grouped.filter((g) => !g.isSemDfd).map((g) => g.key));
|
||||
|
||||
// --- Gerar pedidos modal ---
|
||||
let showGerarModal = $state(false);
|
||||
let seiByDfd = $state<Record<string, string>>({});
|
||||
let gerando = $state(false);
|
||||
|
||||
const canGerar = $derived(
|
||||
isRascunho && items.length > 0 && itensSemDfdCount === 0 && dfdsParaGerar.length > 0
|
||||
);
|
||||
|
||||
function openGerarModal() {
|
||||
if (!canGerar) {
|
||||
if (items.length === 0) toast.error('Adicione itens antes de gerar pedidos.');
|
||||
else if (itensSemDfdCount > 0) toast.error('Atribua um DFD a todos os itens antes de gerar.');
|
||||
return;
|
||||
}
|
||||
const initial: Record<string, string> = {};
|
||||
for (const dfd of dfdsParaGerar) initial[dfd] = '';
|
||||
seiByDfd = initial;
|
||||
showGerarModal = true;
|
||||
}
|
||||
|
||||
function closeGerarModal() {
|
||||
showGerarModal = false;
|
||||
seiByDfd = {};
|
||||
}
|
||||
|
||||
async function confirmarGeracao() {
|
||||
if (!canGerar) return;
|
||||
for (const dfd of dfdsParaGerar) {
|
||||
if (!seiByDfd[dfd]?.trim()) {
|
||||
return toast.error(`Informe o número SEI para o DFD ${dfd}.`);
|
||||
}
|
||||
}
|
||||
|
||||
gerando = true;
|
||||
try {
|
||||
await client.mutation(api.planejamentos.gerarPedidosPorDfd, {
|
||||
planejamentoId,
|
||||
dfds: dfdsParaGerar.map((dfd) => ({ numeroDfd: dfd, numeroSei: seiByDfd[dfd] }))
|
||||
});
|
||||
toast.success('Pedidos gerados.');
|
||||
closeGerarModal();
|
||||
} catch (e) {
|
||||
toast.error((e as Error).message);
|
||||
} finally {
|
||||
gerando = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="container mx-auto p-6">
|
||||
{#if planejamentoQuery.isLoading}
|
||||
<p>Carregando...</p>
|
||||
{:else if planejamentoQuery.error}
|
||||
<p class="text-red-600">{planejamentoQuery.error.message}</p>
|
||||
{:else if planejamento}
|
||||
<div class="mb-6 overflow-hidden rounded-lg bg-white p-6 shadow-md">
|
||||
<div class="flex items-start justify-between gap-4">
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="flex items-center gap-3">
|
||||
<h1 class="truncate text-2xl font-bold text-gray-900">{planejamento.titulo}</h1>
|
||||
<span
|
||||
class="inline-flex rounded-full px-2 py-1 text-xs font-semibold {getStatusColor(
|
||||
planejamento.status
|
||||
)}"
|
||||
>
|
||||
{formatStatus(planejamento.status)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<p class="mt-2 text-sm whitespace-pre-wrap text-gray-700">{planejamento.descricao}</p>
|
||||
|
||||
<div class="mt-4 grid gap-3 md:grid-cols-3">
|
||||
<div class="rounded-lg bg-gray-50 p-3">
|
||||
<div class="text-xs font-semibold text-gray-500">Data</div>
|
||||
<div class="text-sm text-gray-900">{planejamento.data}</div>
|
||||
</div>
|
||||
<div class="rounded-lg bg-gray-50 p-3">
|
||||
<div class="text-xs font-semibold text-gray-500">Responsável</div>
|
||||
<div class="text-sm text-gray-900">{planejamento.responsavelNome}</div>
|
||||
</div>
|
||||
<div class="rounded-lg bg-gray-50 p-3">
|
||||
<div class="text-xs font-semibold text-gray-500">Ação</div>
|
||||
<div class="text-sm text-gray-900">{planejamento.acaoNome || '-'}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex shrink-0 flex-col items-end gap-2">
|
||||
{#if isRascunho}
|
||||
{#if editingHeader}
|
||||
<div class="flex gap-2">
|
||||
<button
|
||||
type="button"
|
||||
class="flex items-center gap-2 rounded bg-green-600 px-3 py-2 text-sm text-white hover:bg-green-700 disabled:opacity-50"
|
||||
onclick={saveHeader}
|
||||
disabled={savingHeader}
|
||||
>
|
||||
<Save size={16} />
|
||||
Salvar
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="rounded bg-gray-200 px-3 py-2 text-sm text-gray-800 hover:bg-gray-300"
|
||||
onclick={cancelEditHeader}
|
||||
disabled={savingHeader}
|
||||
>
|
||||
Cancelar
|
||||
</button>
|
||||
</div>
|
||||
{:else}
|
||||
<button
|
||||
type="button"
|
||||
class="flex items-center gap-2 rounded bg-gray-100 px-3 py-2 text-sm text-gray-800 hover:bg-gray-200"
|
||||
onclick={startEditHeader}
|
||||
>
|
||||
<Edit size={16} />
|
||||
Editar
|
||||
</button>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if editingHeader}
|
||||
<div class="mt-5 rounded-lg border border-gray-200 bg-white p-4">
|
||||
<div class="grid gap-4 md:grid-cols-2">
|
||||
<div class="md:col-span-2">
|
||||
<label class="mb-1 block text-sm font-medium text-gray-700" for="eh_titulo"
|
||||
>Título</label
|
||||
>
|
||||
<input
|
||||
id="eh_titulo"
|
||||
type="text"
|
||||
class="w-full rounded-md border border-gray-300 px-3 py-2 text-sm"
|
||||
bind:value={headerForm.titulo}
|
||||
disabled={savingHeader}
|
||||
/>
|
||||
</div>
|
||||
<div class="md:col-span-2">
|
||||
<label class="mb-1 block text-sm font-medium text-gray-700" for="eh_descricao"
|
||||
>Descrição</label
|
||||
>
|
||||
<textarea
|
||||
id="eh_descricao"
|
||||
rows="4"
|
||||
class="w-full rounded-md border border-gray-300 px-3 py-2 text-sm"
|
||||
bind:value={headerForm.descricao}
|
||||
disabled={savingHeader}
|
||||
></textarea>
|
||||
</div>
|
||||
<div>
|
||||
<label class="mb-1 block text-sm font-medium text-gray-700" for="eh_data">Data</label>
|
||||
<input
|
||||
id="eh_data"
|
||||
type="date"
|
||||
class="w-full rounded-md border border-gray-300 px-3 py-2 text-sm"
|
||||
bind:value={headerForm.data}
|
||||
disabled={savingHeader}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="mb-1 block text-sm font-medium text-gray-700" for="eh_resp"
|
||||
>Responsável</label
|
||||
>
|
||||
<select
|
||||
id="eh_resp"
|
||||
class="w-full rounded-md border border-gray-300 px-3 py-2 text-sm"
|
||||
bind:value={headerForm.responsavelId}
|
||||
disabled={savingHeader || funcionariosQuery.isLoading}
|
||||
>
|
||||
<option value="">Selecione...</option>
|
||||
{#each funcionariosQuery.data || [] as f (f._id)}
|
||||
<option value={f._id}>{f.nome}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
<div class="md:col-span-2">
|
||||
<label class="mb-1 block text-sm font-medium text-gray-700" for="eh_acao"
|
||||
>Ação (opcional)</label
|
||||
>
|
||||
<select
|
||||
id="eh_acao"
|
||||
class="w-full rounded-md border border-gray-300 px-3 py-2 text-sm"
|
||||
bind:value={headerForm.acaoId}
|
||||
disabled={savingHeader || acoesQuery.isLoading}
|
||||
>
|
||||
<option value="">Nenhuma</option>
|
||||
{#each acoesQuery.data || [] as a (a._id)}
|
||||
<option value={a._id}>{a.nome}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Itens -->
|
||||
<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">
|
||||
<div>
|
||||
<h2 class="text-lg font-semibold">Itens</h2>
|
||||
{#if itensSemDfdCount > 0}
|
||||
<p class="mt-1 text-xs text-amber-700">
|
||||
{itensSemDfdCount} item(ns) sem DFD. Para gerar pedidos, todos os itens precisam ter DFD.
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
{#if isRascunho}
|
||||
<button
|
||||
type="button"
|
||||
onclick={openGerarModal}
|
||||
disabled={!canGerar}
|
||||
class="rounded bg-indigo-600 px-3 py-2 text-sm text-white hover:bg-indigo-700 disabled:opacity-50"
|
||||
>
|
||||
Gerar pedidos
|
||||
</button>
|
||||
{/if}
|
||||
{#if isRascunho}
|
||||
<div class="relative">
|
||||
<input
|
||||
type="text"
|
||||
class="w-64 rounded-md border border-gray-300 px-3 py-2 text-sm"
|
||||
placeholder="Buscar objetos..."
|
||||
bind:value={searchQuery}
|
||||
/>
|
||||
{#if searchQuery.length > 0}
|
||||
<div
|
||||
class="absolute z-10 mt-2 w-full rounded-lg border border-gray-200 bg-white shadow-xl"
|
||||
>
|
||||
{#if searchResultsQuery.isLoading}
|
||||
<div class="p-3 text-sm text-gray-500">Buscando...</div>
|
||||
{:else if searchResults.length === 0}
|
||||
<div class="p-3 text-sm text-gray-500">Nenhum objeto encontrado.</div>
|
||||
{:else}
|
||||
<ul class="max-h-64 overflow-y-auto">
|
||||
{#each searchResults as o (o._id)}
|
||||
<li>
|
||||
<button
|
||||
type="button"
|
||||
class="flex w-full items-center justify-between px-4 py-3 text-left transition hover:bg-blue-50"
|
||||
onclick={() => openAddItemModal(o)}
|
||||
>
|
||||
<span class="font-medium text-gray-800">{o.nome}</span>
|
||||
<Plus size={16} class="text-blue-600" />
|
||||
</button>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="p-6">
|
||||
{#if itemsQuery.isLoading}
|
||||
<p class="text-sm text-gray-500">Carregando itens...</p>
|
||||
{:else if items.length === 0}
|
||||
<p class="text-sm text-gray-500">Nenhum item adicionado.</p>
|
||||
{:else}
|
||||
<div class="space-y-4">
|
||||
{#each grouped as group (group.key)}
|
||||
{@const dfd = group.isSemDfd ? null : group.key}
|
||||
{@const pedidoLink = dfd ? pedidosByDfd[dfd] : null}
|
||||
<div class="rounded-lg border border-gray-200">
|
||||
<div class="flex items-center justify-between bg-gray-50 px-4 py-3">
|
||||
<div class="flex flex-col gap-1">
|
||||
<div class="font-semibold text-gray-900">{group.label}</div>
|
||||
{#if pedidoLink?.pedido}
|
||||
<a
|
||||
href={resolve(`/pedidos/${pedidoLink.pedido._id}`)}
|
||||
class="text-xs text-blue-700 hover:underline"
|
||||
>
|
||||
Pedido: {pedidoLink.pedido.numeroSei || pedidoLink.pedido._id} — {formatPedidoStatus(
|
||||
pedidoLink.pedido.status
|
||||
)}
|
||||
</a>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="text-xs text-gray-500">{group.items.length} item(ns)</div>
|
||||
</div>
|
||||
|
||||
<div class="overflow-x-auto">
|
||||
<table class="min-w-full divide-y divide-gray-200">
|
||||
<thead class="bg-white">
|
||||
<tr>
|
||||
<th class="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase"
|
||||
>Objeto</th
|
||||
>
|
||||
<th class="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase"
|
||||
>Qtd</th
|
||||
>
|
||||
<th class="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase"
|
||||
>Valor Est.</th
|
||||
>
|
||||
<th class="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase"
|
||||
>DFD</th
|
||||
>
|
||||
<th class="px-4 py-2 text-right text-xs font-medium text-gray-500 uppercase"
|
||||
>Ações</th
|
||||
>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-200 bg-white">
|
||||
{#each group.items as it (it._id)}
|
||||
<tr class="hover:bg-gray-50">
|
||||
<td class="px-4 py-2 text-sm text-gray-900">
|
||||
{it.objetoNome}
|
||||
{#if it.objetoUnidade}
|
||||
<span class="ml-2 text-xs text-gray-400">({it.objetoUnidade})</span>
|
||||
{/if}
|
||||
</td>
|
||||
<td class="px-4 py-2 text-sm text-gray-700">
|
||||
{#if isRascunho}
|
||||
<input
|
||||
type="number"
|
||||
min="1"
|
||||
class="w-24 rounded border px-2 py-1 text-sm"
|
||||
value={it.quantidade}
|
||||
onchange={(e) =>
|
||||
updateItemField(it._id, {
|
||||
quantidade: parseInt(e.currentTarget.value, 10) || 1
|
||||
})}
|
||||
/>
|
||||
{:else}
|
||||
{it.quantidade}
|
||||
{/if}
|
||||
</td>
|
||||
<td class="px-4 py-2 text-sm text-gray-700">
|
||||
{#if isRascunho}
|
||||
<input
|
||||
type="text"
|
||||
class="w-40 rounded border px-2 py-1 text-sm"
|
||||
value={it.valorEstimado}
|
||||
onblur={(e) =>
|
||||
updateItemField(it._id, { valorEstimado: e.currentTarget.value })}
|
||||
/>
|
||||
{:else}
|
||||
{it.valorEstimado}
|
||||
{/if}
|
||||
</td>
|
||||
<td class="px-4 py-2 text-sm text-gray-700">
|
||||
{#if isRascunho}
|
||||
<input
|
||||
type="text"
|
||||
class="w-32 rounded border px-2 py-1 text-sm"
|
||||
value={it.numeroDfd || ''}
|
||||
placeholder="(opcional)"
|
||||
onblur={(e) => {
|
||||
const v = e.currentTarget.value.trim();
|
||||
updateItemField(it._id, { numeroDfd: v ? v : null });
|
||||
}}
|
||||
/>
|
||||
{:else}
|
||||
{it.numeroDfd || '-'}
|
||||
{/if}
|
||||
</td>
|
||||
<td class="px-4 py-2 text-right">
|
||||
{#if isRascunho}
|
||||
<button
|
||||
type="button"
|
||||
class="rounded bg-red-100 p-2 text-red-700 hover:bg-red-200"
|
||||
onclick={() => removeItem(it._id)}
|
||||
title="Remover item"
|
||||
>
|
||||
<Trash2 size={16} />
|
||||
</button>
|
||||
{:else}
|
||||
<span class="text-xs text-gray-400">—</span>
|
||||
{/if}
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Pedidos -->
|
||||
<div class="overflow-hidden rounded-lg bg-white shadow-md">
|
||||
<div class="border-b border-gray-200 px-6 py-4">
|
||||
<h2 class="text-lg font-semibold">Pedidos gerados</h2>
|
||||
</div>
|
||||
<div class="p-6">
|
||||
{#if pedidosQuery.isLoading}
|
||||
<p class="text-sm text-gray-500">Carregando pedidos...</p>
|
||||
{:else if pedidosLinks.length === 0}
|
||||
<p class="text-sm text-gray-500">Nenhum pedido gerado ainda.</p>
|
||||
{:else}
|
||||
<div class="space-y-4">
|
||||
{#each pedidosLinks as row (row._id)}
|
||||
<div class="rounded-lg border border-gray-200 p-4">
|
||||
<div class="flex flex-wrap items-center justify-between gap-3">
|
||||
<div class="space-y-1">
|
||||
<div class="text-sm font-semibold text-gray-900">DFD: {row.numeroDfd}</div>
|
||||
{#if row.pedido}
|
||||
<a
|
||||
class="text-sm text-blue-700 hover:underline"
|
||||
href={resolve(`/pedidos/${row.pedido._id}`)}
|
||||
>
|
||||
Pedido: {row.pedido.numeroSei || row.pedido._id}
|
||||
</a>
|
||||
{/if}
|
||||
</div>
|
||||
{#if row.pedido}
|
||||
<span
|
||||
class="inline-flex rounded-full px-2 py-1 text-xs font-semibold {getPedidoStatusColor(
|
||||
row.pedido.status
|
||||
)}"
|
||||
>
|
||||
{formatPedidoStatus(row.pedido.status)}
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if row.lastHistory && row.lastHistory.length > 0}
|
||||
<div class="mt-3">
|
||||
<div class="mb-2 text-xs font-semibold text-gray-500">Últimas ações</div>
|
||||
<ul class="space-y-1 text-xs text-gray-700">
|
||||
{#each row.lastHistory as h (h._id)}
|
||||
<li class="flex items-center justify-between gap-2">
|
||||
<span class="truncate">{h.usuarioNome}: {h.acao}</span>
|
||||
<span class="shrink-0 text-gray-400"
|
||||
>{new Date(h.data).toLocaleString('pt-BR')}</span
|
||||
>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Add item modal -->
|
||||
{#if showAddItemModal && addItemConfig.objeto}
|
||||
<div
|
||||
class="fixed inset-0 z-50 flex h-full w-full items-center justify-center overflow-y-auto bg-black/40 p-4"
|
||||
>
|
||||
<div class="relative w-full max-w-lg rounded-xl bg-white p-6 shadow-2xl">
|
||||
<button
|
||||
type="button"
|
||||
onclick={closeAddItemModal}
|
||||
class="absolute top-4 right-4 rounded-lg p-1 text-gray-400 transition hover:bg-gray-100 hover:text-gray-600"
|
||||
aria-label="Fechar"
|
||||
disabled={addingItem}
|
||||
>
|
||||
<X size={24} />
|
||||
</button>
|
||||
|
||||
<h3 class="mb-4 text-xl font-bold text-gray-900">Adicionar item</h3>
|
||||
|
||||
<div class="mb-4 rounded-lg bg-blue-50 p-4">
|
||||
<p class="font-semibold text-gray-900">{addItemConfig.objeto.nome}</p>
|
||||
<p class="text-sm text-gray-600">Unidade: {addItemConfig.objeto.unidade}</p>
|
||||
</div>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label class="mb-2 block text-sm font-medium text-gray-700" for="ai_qtd"
|
||||
>Quantidade</label
|
||||
>
|
||||
<input
|
||||
id="ai_qtd"
|
||||
type="number"
|
||||
min="1"
|
||||
class="w-full rounded-lg border border-gray-300 px-4 py-2.5"
|
||||
bind:value={addItemConfig.quantidade}
|
||||
disabled={addingItem}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="mb-2 block text-sm font-medium text-gray-700" for="ai_valor"
|
||||
>Valor estimado</label
|
||||
>
|
||||
<input
|
||||
id="ai_valor"
|
||||
type="text"
|
||||
class="w-full rounded-lg border border-gray-300 px-4 py-2.5"
|
||||
bind:value={addItemConfig.valorEstimado}
|
||||
disabled={addingItem}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="mb-2 block text-sm font-medium text-gray-700" for="ai_dfd"
|
||||
>Número DFD (opcional)</label
|
||||
>
|
||||
<input
|
||||
id="ai_dfd"
|
||||
type="text"
|
||||
class="w-full rounded-lg border border-gray-300 px-4 py-2.5"
|
||||
bind:value={addItemConfig.numeroDfd}
|
||||
disabled={addingItem}
|
||||
placeholder="Ex: 123/2025"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-6 flex justify-end gap-3">
|
||||
<button
|
||||
type="button"
|
||||
onclick={closeAddItemModal}
|
||||
class="rounded-lg bg-gray-200 px-5 py-2.5 font-semibold text-gray-800 hover:bg-gray-300"
|
||||
disabled={addingItem}
|
||||
>
|
||||
Cancelar
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onclick={confirmAddItem}
|
||||
class="rounded-lg bg-blue-600 px-5 py-2.5 font-semibold text-white hover:bg-blue-700 disabled:opacity-50"
|
||||
disabled={addingItem}
|
||||
>
|
||||
{addingItem ? 'Adicionando...' : 'Adicionar'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Gerar pedidos modal -->
|
||||
{#if showGerarModal}
|
||||
<div
|
||||
class="fixed inset-0 z-50 flex h-full w-full items-center justify-center overflow-y-auto bg-black/40 p-4"
|
||||
>
|
||||
<div class="relative w-full max-w-2xl rounded-xl bg-white p-6 shadow-2xl">
|
||||
<button
|
||||
type="button"
|
||||
onclick={closeGerarModal}
|
||||
class="absolute top-4 right-4 rounded-lg p-1 text-gray-400 transition hover:bg-gray-100 hover:text-gray-600"
|
||||
aria-label="Fechar"
|
||||
disabled={gerando}
|
||||
>
|
||||
<X size={24} />
|
||||
</button>
|
||||
|
||||
<h3 class="mb-2 text-xl font-bold text-gray-900">Gerar pedidos</h3>
|
||||
<p class="mb-4 text-sm text-gray-600">
|
||||
Será criado <strong>1 pedido por DFD</strong>. Informe o número SEI de cada pedido.
|
||||
</p>
|
||||
|
||||
<div class="space-y-3">
|
||||
{#each dfdsParaGerar as dfd (dfd)}
|
||||
<div class="grid gap-3 rounded-lg border border-gray-200 p-4 md:grid-cols-3">
|
||||
<div class="md:col-span-1">
|
||||
<div class="text-xs font-semibold text-gray-500">DFD</div>
|
||||
<div class="text-sm font-semibold text-gray-900">{dfd}</div>
|
||||
</div>
|
||||
<div class="md:col-span-2">
|
||||
<label class="mb-1 block text-xs font-semibold text-gray-500" for={`sei_${dfd}`}
|
||||
>Número SEI</label
|
||||
>
|
||||
<input
|
||||
id={`sei_${dfd}`}
|
||||
type="text"
|
||||
class="w-full rounded-md border border-gray-300 px-3 py-2 text-sm"
|
||||
bind:value={seiByDfd[dfd]}
|
||||
disabled={gerando}
|
||||
placeholder="Ex: 12345.000000/2025-00"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<div class="mt-6 flex justify-end gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onclick={closeGerarModal}
|
||||
class="rounded-lg bg-gray-200 px-5 py-2.5 font-semibold text-gray-800 hover:bg-gray-300"
|
||||
disabled={gerando}
|
||||
>
|
||||
Cancelar
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onclick={confirmarGeracao}
|
||||
class="rounded-lg bg-indigo-600 px-5 py-2.5 font-semibold text-white hover:bg-indigo-700 disabled:opacity-50"
|
||||
disabled={gerando}
|
||||
>
|
||||
{gerando ? 'Gerando...' : 'Confirmar e gerar'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
Reference in New Issue
Block a user