Feat pedidos #70
@@ -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>
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user