Feat pedidos #70

Merged
killer-cf merged 3 commits from feat-pedidos into master 2025-12-22 17:31:49 +00:00
14 changed files with 999 additions and 2111 deletions
Showing only changes of commit 0a4be24655 - Show all commits

View File

@@ -6,7 +6,7 @@
import PageShell from '$lib/components/layout/PageShell.svelte'; import PageShell from '$lib/components/layout/PageShell.svelte';
import TableCard from '$lib/components/ui/TableCard.svelte'; import TableCard from '$lib/components/ui/TableCard.svelte';
import { useConvexClient, useQuery } from 'convex-svelte'; import { useConvexClient, useQuery } from 'convex-svelte';
import { ClipboardList, Eye, Plus, X } from 'lucide-svelte'; import { ClipboardList, Eye, Plus, X, Copy } from 'lucide-svelte';
import { resolve } from '$app/paths'; import { resolve } from '$app/paths';
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import { toast } from 'svelte-sonner'; import { toast } from 'svelte-sonner';
@@ -60,11 +60,31 @@
descricao: '', descricao: '',
data: '', data: '',
responsavelId: '' as string, responsavelId: '' as string,
acaoId: '' as string acaoId: '' as string,
sourcePlanningId: '' as string
}); });
function openCreate() { function openCreate() {
form = { titulo: '', descricao: '', data: '', responsavelId: '', acaoId: '' }; form = {
titulo: '',
descricao: '',
data: '',
responsavelId: '',
acaoId: '',
sourcePlanningId: ''
};
showCreate = true;
}
function openClone(planning: (typeof planejamentos)[0]) {
form = {
titulo: `${planning.titulo} (Cópia)`,
descricao: planning.descricao,
data: planning.data,
responsavelId: planning.responsavelId,
acaoId: planning.acaoId || '',
sourcePlanningId: planning._id
};
showCreate = true; showCreate = true;
} }
@@ -85,7 +105,11 @@
descricao: form.descricao, descricao: form.descricao,
data: form.data, data: form.data,
responsavelId: form.responsavelId as Id<'funcionarios'>, responsavelId: form.responsavelId as Id<'funcionarios'>,
acaoId: form.acaoId ? (form.acaoId as Id<'acoes'>) : undefined
acaoId: form.acaoId ? (form.acaoId as Id<'acoes'>) : undefined,
sourcePlanningId: form.sourcePlanningId
? (form.sourcePlanningId as Id<'planejamentosPedidos'>)
: undefined
}); });
toast.success('Planejamento criado.'); toast.success('Planejamento criado.');
showCreate = false; showCreate = false;
@@ -184,14 +208,24 @@
</span> </span>
</td> </td>
<td class="text-right whitespace-nowrap"> <td class="text-right whitespace-nowrap">
<a <div class="flex items-center justify-end gap-2">
href={resolve(`/pedidos/planejamento/${p._id}`)} <a
class="btn btn-ghost btn-sm gap-2" href={resolve(`/pedidos/planejamento/${p._id}`)}
aria-label="Abrir planejamento" class="btn btn-ghost btn-sm gap-2"
> aria-label="Abrir planejamento"
<Eye class="h-4 w-4" /> >
Abrir <Eye class="h-4 w-4" />
</a> Abrir
</a>
<button
class="btn btn-ghost btn-sm gap-2"
onclick={() => openClone(p)}
aria-label="Clonar planejamento"
>
<Copy class="h-4 w-4" />
Clonar
</button>
</div>
</td> </td>
</tr> </tr>
{/each} {/each}
@@ -214,7 +248,9 @@
<X class="h-5 w-5" /> <X class="h-5 w-5" />
</button> </button>
<h3 class="text-lg font-bold">Novo planejamento</h3> <h3 class="text-lg font-bold">
{form.sourcePlanningId ? 'Clonar planejamento' : 'Novo planejamento'}
</h3>
<div class="mt-6 grid grid-cols-1 gap-4 md:grid-cols-2"> <div class="mt-6 grid grid-cols-1 gap-4 md:grid-cols-2">
<div class="form-control w-full md:col-span-2"> <div class="form-control w-full md:col-span-2">
@@ -299,7 +335,13 @@
{#if creating} {#if creating}
<span class="loading loading-spinner loading-sm"></span> <span class="loading loading-spinner loading-sm"></span>
{/if} {/if}
{creating ? 'Criando...' : 'Criar'} {creating
? form.sourcePlanningId
? 'Clonando...'
: 'Criando...'
: form.sourcePlanningId
? 'Clonar'
: 'Criar'}
</button> </button>
</div> </div>
</div> </div>

View File

@@ -7,8 +7,9 @@
import { useConvexClient, useQuery } from 'convex-svelte'; import { useConvexClient, useQuery } from 'convex-svelte';
import { page } from '$app/state'; import { page } from '$app/state';
import { resolve } from '$app/paths'; import { resolve } from '$app/paths';
import { goto } from '$app/navigation';
import { toast } from 'svelte-sonner'; import { toast } from 'svelte-sonner';
import { Plus, Trash2, X, Save, Edit } from 'lucide-svelte'; import { Plus, Trash2, X, Save, Edit, Copy } from 'lucide-svelte';
const client = useConvexClient(); const client = useConvexClient();
const planejamentoId = $derived(page.params.id as Id<'planejamentosPedidos'>); const planejamentoId = $derived(page.params.id as Id<'planejamentosPedidos'>);
@@ -323,6 +324,63 @@
gerando = false; gerando = false;
} }
} }
// --- Clone/Create Modal logic (duplicated from list page for now) ---
let showCreate = $state(false);
let creating = $state(false);
let createForm = $state({
titulo: '',
descricao: '',
data: '',
responsavelId: '' as string,
acaoId: '' as string,
sourcePlanningId: '' as string
});
function openClone() {
if (!planejamento) return;
createForm = {
titulo: `${planejamento.titulo} (Cópia)`,
descricao: planejamento.descricao,
data: planejamento.data,
responsavelId: planejamento.responsavelId,
acaoId: planejamento.acaoId || '',
sourcePlanningId: planejamento._id
};
showCreate = true;
}
function closeCreate() {
showCreate = false;
}
async function handleCreate() {
if (!createForm.titulo.trim()) return toast.error('Informe um título.');
if (!createForm.descricao.trim()) return toast.error('Informe uma descrição.');
if (!createForm.data.trim()) return toast.error('Informe uma data.');
if (!createForm.responsavelId) return toast.error('Selecione um responsável.');
creating = true;
try {
const id = await client.mutation(api.planejamentos.create, {
titulo: createForm.titulo,
descricao: createForm.descricao,
data: createForm.data,
responsavelId: createForm.responsavelId as Id<'funcionarios'>,
acaoId: createForm.acaoId ? (createForm.acaoId as Id<'acoes'>) : undefined,
sourcePlanningId: createForm.sourcePlanningId
? (createForm.sourcePlanningId as Id<'planejamentosPedidos'>)
: undefined
});
toast.success('Planejamento clonado com sucesso.');
showCreate = false;
await goto(resolve(`/pedidos/planejamento/${id}`));
} catch (e) {
toast.error((e as Error).message);
} finally {
creating = false;
}
}
</script> </script>
<PageShell> <PageShell>
@@ -375,6 +433,10 @@
</div> </div>
<div class="flex shrink-0 flex-col items-end gap-2"> <div class="flex shrink-0 flex-col items-end gap-2">
<button class="btn btn-ghost btn-sm gap-2" onclick={openClone}>
<Copy class="h-4 w-4" />
Clonar
</button>
{#if isRascunho} {#if isRascunho}
{#if editingHeader} {#if editingHeader}
<div class="flex gap-2"> <div class="flex gap-2">
@@ -803,9 +865,9 @@
</div> </div>
<div class="modal-action"> <div class="modal-action">
<button type="button" class="btn" onclick={closeAddItemModal} disabled={addingItem}> <button type="button" class="btn" onclick={closeAddItemModal} disabled={addingItem}
Cancelar >Cancelar</button
</button> >
<button <button
type="button" type="button"
class="btn btn-primary" class="btn btn-primary"
@@ -815,15 +877,123 @@
{#if addingItem} {#if addingItem}
<span class="loading loading-spinner loading-sm"></span> <span class="loading loading-spinner loading-sm"></span>
{/if} {/if}
{addingItem ? 'Adicionando...' : 'Adicionar'} Adicionar
</button> </button>
</div> </div>
</div> </div>
<button <button type="button" class="modal-backdrop" onclick={closeAddItemModal} aria-label="Fechar"
type="button" ></button>
class="modal-backdrop" </div>
onclick={closeAddItemModal} {/if}
aria-label="Fechar modal"
{#if showCreate}
<div class="modal modal-open">
<div class="modal-box max-w-2xl">
<button
type="button"
class="btn btn-sm btn-circle absolute top-2 right-2"
onclick={closeCreate}
aria-label="Fechar modal"
disabled={creating}
>
<X class="h-5 w-5" />
</button>
<h3 class="text-lg font-bold">Clonar planejamento</h3>
<div class="mt-6 grid grid-cols-1 gap-4 md:grid-cols-2">
<div class="form-control w-full md:col-span-2">
<label class="label" for="c_titulo">
<span class="label-text font-semibold">Título</span>
</label>
<input
id="c_titulo"
type="text"
class="input input-bordered focus:input-primary w-full"
bind:value={createForm.titulo}
disabled={creating}
/>
</div>
<div class="form-control w-full md:col-span-2">
<label class="label" for="c_descricao">
<span class="label-text font-semibold">Descrição</span>
</label>
<textarea
id="c_descricao"
class="textarea textarea-bordered focus:textarea-primary w-full"
rows="4"
bind:value={createForm.descricao}
disabled={creating}
></textarea>
</div>
<div class="form-control w-full">
<label class="label" for="c_data">
<span class="label-text font-semibold">Data</span>
</label>
<input
id="c_data"
type="date"
class="input input-bordered focus:input-primary w-full"
bind:value={createForm.data}
disabled={creating}
/>
</div>
<div class="form-control w-full">
<label class="label" for="c_responsavel">
<span class="label-text font-semibold">Responsável</span>
</label>
<select
id="c_responsavel"
class="select select-bordered focus:select-primary w-full"
bind:value={createForm.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="form-control w-full md:col-span-2">
<label class="label" for="c_acao">
<span class="label-text font-semibold">Ação (opcional)</span>
</label>
<select
id="c_acao"
class="select select-bordered focus:select-primary w-full"
bind:value={createForm.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="modal-action">
<button type="button" class="btn" onclick={closeCreate} disabled={creating}
>Cancelar</button
>
<button
type="button"
class="btn btn-primary"
onclick={handleCreate}
disabled={creating}
>
{#if creating}
<span class="loading loading-spinner loading-sm"></span>
{/if}
Clonar
</button>
</div>
</div>
<button type="button" class="modal-backdrop" onclick={closeCreate} aria-label="Fechar"
></button> ></button>
</div> </div>
{/if} {/if}

View File

@@ -177,7 +177,8 @@ export const create = mutation({
descricao: v.string(), descricao: v.string(),
data: v.string(), data: v.string(),
responsavelId: v.id('funcionarios'), responsavelId: v.id('funcionarios'),
acaoId: v.optional(v.id('acoes')) acaoId: v.optional(v.id('acoes')),
sourcePlanningId: v.optional(v.id('planejamentosPedidos'))
}, },
returns: v.id('planejamentosPedidos'), returns: v.id('planejamentosPedidos'),
handler: async (ctx, args) => { handler: async (ctx, args) => {
@@ -192,7 +193,7 @@ export const create = mutation({
if (!descricao) throw new Error('Informe uma descrição.'); if (!descricao) throw new Error('Informe uma descrição.');
if (!data) throw new Error('Informe uma data.'); if (!data) throw new Error('Informe uma data.');
return await ctx.db.insert('planejamentosPedidos', { const newItemId = await ctx.db.insert('planejamentosPedidos', {
titulo, titulo,
descricao, descricao,
data, data,
@@ -203,6 +204,30 @@ export const create = mutation({
criadoEm: now, criadoEm: now,
atualizadoEm: now atualizadoEm: now
}); });
const sourcePlanningId = args.sourcePlanningId;
if (sourcePlanningId) {
const sourceItems = await ctx.db
.query('planejamentoItens')
.withIndex('by_planejamentoId', (q) => q.eq('planejamentoId', sourcePlanningId))
.collect();
for (const item of sourceItems) {
await ctx.db.insert('planejamentoItens', {
planejamentoId: newItemId,
objetoId: item.objetoId,
quantidade: item.quantidade,
valorEstimado: item.valorEstimado,
numeroDfd: item.numeroDfd,
// Não copiamos o pedidoId pois é um novo planejamento
criadoEm: now,
atualizadoEm: now
});
}
}
return newItemId;
} }
}); });