feat: Add 'atas' (minutes/records) management feature, and implement various improvements across UI, backend logic, and authentication.
This commit is contained in:
326
apps/web/src/routes/(dashboard)/compras/objetos/+page.svelte
Normal file
326
apps/web/src/routes/(dashboard)/compras/objetos/+page.svelte
Normal file
@@ -0,0 +1,326 @@
|
||||
<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 { Pencil, Plus, Trash2, X } from 'lucide-svelte';
|
||||
import { maskCurrencyBRL } from '$lib/utils/masks';
|
||||
|
||||
const client = useConvexClient();
|
||||
|
||||
// Reactive query
|
||||
const objetosQuery = useQuery(api.objetos.list, {});
|
||||
const objetos = $derived(objetosQuery.data || []);
|
||||
const loading = $derived(objetosQuery.isLoading);
|
||||
const error = $derived(objetosQuery.error?.message || null);
|
||||
|
||||
// Modal state
|
||||
let showModal = $state(false);
|
||||
let editingId: string | null = $state(null);
|
||||
let formData = $state({
|
||||
nome: '',
|
||||
valorEstimado: '',
|
||||
tipo: 'material' as 'material' | 'servico',
|
||||
codigoEfisco: '',
|
||||
codigoCatmat: '',
|
||||
codigoCatserv: '',
|
||||
unidade: ''
|
||||
});
|
||||
let saving = $state(false);
|
||||
|
||||
function openModal(objeto?: Doc<'objetos'>) {
|
||||
if (objeto) {
|
||||
editingId = objeto._id;
|
||||
formData = {
|
||||
nome: objeto.nome,
|
||||
valorEstimado: maskCurrencyBRL(objeto.valorEstimado || ''),
|
||||
tipo: objeto.tipo,
|
||||
codigoEfisco: objeto.codigoEfisco || '',
|
||||
codigoCatmat: objeto.codigoCatmat || '',
|
||||
codigoCatserv: objeto.codigoCatserv || '',
|
||||
unidade: objeto.unidade || ''
|
||||
};
|
||||
} else {
|
||||
editingId = null;
|
||||
formData = {
|
||||
nome: '',
|
||||
valorEstimado: '',
|
||||
tipo: 'material',
|
||||
codigoEfisco: '',
|
||||
codigoCatmat: '',
|
||||
codigoCatserv: '',
|
||||
unidade: ''
|
||||
};
|
||||
}
|
||||
showModal = true;
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
showModal = false;
|
||||
editingId = null;
|
||||
}
|
||||
|
||||
async function handleSubmit(e: Event) {
|
||||
e.preventDefault();
|
||||
saving = true;
|
||||
try {
|
||||
const payload = {
|
||||
nome: formData.nome,
|
||||
valorEstimado: formData.valorEstimado,
|
||||
tipo: formData.tipo,
|
||||
codigoEfisco: formData.codigoEfisco,
|
||||
codigoCatmat: formData.codigoCatmat || undefined,
|
||||
codigoCatserv: formData.codigoCatserv || undefined,
|
||||
unidade: formData.unidade
|
||||
};
|
||||
|
||||
if (editingId) {
|
||||
await client.mutation(api.objetos.update, {
|
||||
id: editingId as Id<'objetos'>,
|
||||
...payload
|
||||
});
|
||||
} else {
|
||||
await client.mutation(api.objetos.create, payload);
|
||||
}
|
||||
closeModal();
|
||||
} catch (e) {
|
||||
alert('Erro ao salvar: ' + (e as Error).message);
|
||||
} finally {
|
||||
saving = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDelete(id: Id<'objetos'>) {
|
||||
if (!confirm('Tem certeza que deseja excluir este objeto?')) return;
|
||||
try {
|
||||
await client.mutation(api.objetos.remove, { id });
|
||||
} catch (e) {
|
||||
alert('Erro ao excluir: ' + (e as Error).message);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="container mx-auto p-6">
|
||||
<div class="mb-6 flex items-center justify-between">
|
||||
<h1 class="text-2xl font-bold">Objetos</h1>
|
||||
<button
|
||||
onclick={() => openModal()}
|
||||
class="flex items-center gap-2 rounded-md bg-blue-600 px-4 py-2 text-white hover:bg-blue-700"
|
||||
>
|
||||
<Plus size={20} />
|
||||
Novo Objeto
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{#if loading}
|
||||
<p>Carregando...</p>
|
||||
{:else if error}
|
||||
<p class="text-red-600">{error}</p>
|
||||
{: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"
|
||||
>Nome</th
|
||||
>
|
||||
<th
|
||||
class="px-6 py-3 text-left text-xs font-medium tracking-wider text-gray-500 uppercase"
|
||||
>Tipo</th
|
||||
>
|
||||
<th
|
||||
class="px-6 py-3 text-left text-xs font-medium tracking-wider text-gray-500 uppercase"
|
||||
>Unidade</th
|
||||
>
|
||||
<th
|
||||
class="px-6 py-3 text-left text-xs font-medium tracking-wider text-gray-500 uppercase"
|
||||
>Valor Estimado</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 objetos as objeto (objeto._id)}
|
||||
<tr>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<div class="flex flex-col">
|
||||
<span class="font-medium">{objeto.nome}</span>
|
||||
<span class="text-xs text-gray-500">
|
||||
Efisco: {objeto.codigoEfisco}
|
||||
{#if objeto.codigoCatmat}
|
||||
| Catmat: {objeto.codigoCatmat}{/if}
|
||||
{#if objeto.codigoCatserv}
|
||||
| Catserv: {objeto.codigoCatserv}{/if}
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<span
|
||||
class="inline-flex rounded-full px-2 text-xs leading-5 font-semibold
|
||||
{objeto.tipo === 'servico'
|
||||
? 'bg-green-100 text-green-800'
|
||||
: 'bg-blue-100 text-blue-800'}"
|
||||
>
|
||||
{objeto.tipo === 'material' ? 'Material' : 'Serviço'}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">{objeto.unidade}</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
{maskCurrencyBRL(objeto.valorEstimado) || 'R$ 0,00'}
|
||||
</td>
|
||||
<td class="px-6 py-4 text-right text-sm font-medium whitespace-nowrap">
|
||||
<button
|
||||
onclick={() => openModal(objeto)}
|
||||
class="mr-4 text-indigo-600 hover:text-indigo-900"
|
||||
>
|
||||
<Pencil size={18} />
|
||||
</button>
|
||||
<button
|
||||
onclick={() => handleDelete(objeto._id)}
|
||||
class="text-red-600 hover:text-red-900"
|
||||
>
|
||||
<Trash2 size={18} />
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
{#if objetos.length === 0}
|
||||
<tr>
|
||||
<td colspan="5" class="px-6 py-4 text-center text-gray-500"
|
||||
>Nenhum objeto cadastrado.</td
|
||||
>
|
||||
</tr>
|
||||
{/if}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if showModal}
|
||||
<div
|
||||
class="fixed inset-0 z-50 flex h-full w-full items-center justify-center overflow-y-auto bg-black/40"
|
||||
>
|
||||
<div class="relative w-full max-w-md rounded-lg bg-white p-8 shadow-xl">
|
||||
<button
|
||||
onclick={closeModal}
|
||||
class="absolute top-4 right-4 text-gray-400 hover:text-gray-600"
|
||||
>
|
||||
<X size={24} />
|
||||
</button>
|
||||
<h2 class="mb-6 text-xl font-bold">{editingId ? 'Editar' : 'Novo'} Objeto</h2>
|
||||
|
||||
<form onsubmit={handleSubmit}>
|
||||
<div class="mb-4">
|
||||
<label class="mb-2 block text-sm font-bold text-gray-700" for="nome"> Nome </label>
|
||||
<input
|
||||
class="focus:shadow-outline w-full appearance-none rounded border px-3 py-2 leading-tight text-gray-700 shadow focus:outline-none"
|
||||
id="nome"
|
||||
type="text"
|
||||
bind:value={formData.nome}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="mb-4 grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="mb-2 block text-sm font-bold text-gray-700" for="tipo"> Tipo </label>
|
||||
<select
|
||||
class="focus:shadow-outline w-full rounded border px-3 py-2 leading-tight text-gray-700 shadow focus:outline-none"
|
||||
id="tipo"
|
||||
bind:value={formData.tipo}
|
||||
>
|
||||
<option value="material">Material</option>
|
||||
<option value="servico">Serviço</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="mb-2 block text-sm font-bold text-gray-700" for="unidade">
|
||||
Unidade
|
||||
</label>
|
||||
<input
|
||||
class="focus:shadow-outline w-full appearance-none rounded border px-3 py-2 leading-tight text-gray-700 shadow focus:outline-none"
|
||||
id="unidade"
|
||||
type="text"
|
||||
bind:value={formData.unidade}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label class="mb-2 block text-sm font-bold text-gray-700" for="codigoEfisco">
|
||||
Código Efisco
|
||||
</label>
|
||||
<input
|
||||
class="focus:shadow-outline w-full appearance-none rounded border px-3 py-2 leading-tight text-gray-700 shadow focus:outline-none"
|
||||
id="codigoEfisco"
|
||||
type="text"
|
||||
bind:value={formData.codigoEfisco}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
{#if formData.tipo === 'material'}
|
||||
<div class="mb-4">
|
||||
<label class="mb-2 block text-sm font-bold text-gray-700" for="codigoCatmat">
|
||||
Código Catmat
|
||||
</label>
|
||||
<input
|
||||
class="focus:shadow-outline w-full appearance-none rounded border px-3 py-2 leading-tight text-gray-700 shadow focus:outline-none"
|
||||
id="codigoCatmat"
|
||||
type="text"
|
||||
bind:value={formData.codigoCatmat}
|
||||
/>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="mb-4">
|
||||
<label class="mb-2 block text-sm font-bold text-gray-700" for="codigoCatserv">
|
||||
Código Catserv
|
||||
</label>
|
||||
<input
|
||||
class="focus:shadow-outline w-full appearance-none rounded border px-3 py-2 leading-tight text-gray-700 shadow focus:outline-none"
|
||||
id="codigoCatserv"
|
||||
type="text"
|
||||
bind:value={formData.codigoCatserv}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="mb-6">
|
||||
<label class="mb-2 block text-sm font-bold text-gray-700" for="valor">
|
||||
Valor Estimado
|
||||
</label>
|
||||
<input
|
||||
class="focus:shadow-outline w-full appearance-none rounded border px-3 py-2 leading-tight text-gray-700 shadow focus:outline-none"
|
||||
id="valor"
|
||||
type="text"
|
||||
bind:value={formData.valorEstimado}
|
||||
oninput={(e) => (formData.valorEstimado = maskCurrencyBRL(e.currentTarget.value))}
|
||||
placeholder="R$ 0,00"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-end">
|
||||
<button
|
||||
type="button"
|
||||
onclick={closeModal}
|
||||
class="mr-2 rounded bg-gray-300 px-4 py-2 font-bold text-gray-800 hover:bg-gray-400"
|
||||
>
|
||||
Cancelar
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={saving}
|
||||
class="focus:shadow-outline rounded bg-blue-600 px-4 py-2 font-bold text-white hover:bg-blue-700 focus:outline-none disabled:opacity-50"
|
||||
>
|
||||
{saving ? 'Salvando...' : 'Salvar'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
Reference in New Issue
Block a user