Refactor FileUpload component and improve type safety

- Rename imported File icon to FileIcon to avoid naming conflicts
- Update onUpload type to use globalThis.File
- Reformat loadExistingFile and related code for better readability
- Add stricter typing for funcionarioId and related data in documentos
  and editar pages
- Improve error handling and response validation in file upload logic
- Add keyed each blocks for better Svelte list rendering stability
- Fix minor formatting issues in breadcrumb links
This commit is contained in:
2025-11-13 00:12:16 -03:00
parent bd574aedc0
commit 4ffa403c46
3 changed files with 81 additions and 74 deletions

View File

@@ -3,7 +3,7 @@
import { import {
ExternalLink, ExternalLink,
FileText, FileText,
File, File as FileIcon,
Upload, Upload,
Trash2, Trash2,
Eye, Eye,
@@ -16,7 +16,7 @@
value?: string; // storageId value?: string; // storageId
disabled?: boolean; disabled?: boolean;
required?: boolean; required?: boolean;
onUpload: (file: File) => Promise<void>; onUpload: (file: globalThis.File) => Promise<void>;
onRemove: () => Promise<void>; onRemove: () => Promise<void>;
} }
@@ -60,27 +60,22 @@
try { try {
const url = await client.storage.getUrl(storageId as any); const url = await client.storage.getUrl(storageId as any);
if (url) { if (url) {
fileUrl = url; async function handleFileSelect(event: Event) {
fileName = "Documento anexado"; const target = event.target as HTMLInputElement;
const file = target.files?.[0];
// Detectar tipo pelo URL ou assumir PDF // Detectar tipo pelo URL ou assumir PDF
if (url.includes(".pdf") || url.includes("application/pdf")) { if (url.includes('.pdf') || url.includes('application/pdf')) {
fileType = "application/pdf"; fileType = 'application/pdf';
} else { } else {
fileType = "image/jpeg"; fileType = 'image/jpeg';
previewUrl = url; // Para imagens, a URL serve como preview previewUrl = url; // Para imagens, a URL serve como preview
} }
} }
} catch (err) { } catch (err) {
console.error("Erro ao carregar arquivo existente:", err); console.error('Erro ao carregar arquivo existente:', err);
} }
} }
async function handleFileSelect(event: Event) {
const target = event.target as HTMLInputElement;
const file = target.files?.[0];
if (!file) return;
error = null; error = null;
// Validate file size // Validate file size
@@ -188,28 +183,18 @@
/> />
{#if value || fileName} {#if value || fileName}
<div <div class="border-base-300 bg-base-100 flex items-center gap-2 rounded-lg border p-3">
class="flex items-center gap-2 p-3 border border-base-300 rounded-lg bg-base-100"
>
<!-- Preview --> <!-- Preview -->
<div class="shrink-0"> <div class="shrink-0">
{#if previewUrl} {#if previewUrl}
<img <img src={previewUrl} alt="Preview" class="h-12 w-12 rounded object-cover" />
src={previewUrl} {:else if fileType === 'application/pdf' || fileName.endsWith('.pdf')}
alt="Preview" <div class="bg-error/10 flex h-12 w-12 items-center justify-center rounded">
class="w-12 h-12 object-cover rounded" <FileText class="text-error h-6 w-6" strokeWidth={2} />
/>
{:else if fileType === "application/pdf" || fileName.endsWith(".pdf")}
<div
class="w-12 h-12 bg-error/10 rounded flex items-center justify-center"
>
<FileText class="h-6 w-6 text-error" strokeWidth={2} />
</div> </div>
{:else} {:else}
<div <div class="bg-success/10 flex h-12 w-12 items-center justify-center rounded">
class="w-12 h-12 bg-success/10 rounded flex items-center justify-center" <File class="text-success h-6 w-6" strokeWidth={2} />
>
<File class="h-6 w-6 text-success" strokeWidth={2} />
</div> </div>
{/if} {/if}
</div> </div>

View File

@@ -11,15 +11,16 @@
categoriasDocumentos, categoriasDocumentos,
getDocumentosByCategoria getDocumentosByCategoria
} from '$lib/utils/documentos'; } from '$lib/utils/documentos';
import type { Id, Doc } from '@sgse-app/backend/convex/_generated/dataModel';
const client = useConvexClient(); const client = useConvexClient();
let funcionarioId = $derived($page.params.funcionarioId as string); let funcionarioId = $derived($page.params.funcionarioId as Id<'funcionarios'>);
let funcionario = $state<any>(null); let funcionario = $state<Doc<'funcionarios'> | null>(null);
let documentosStorage = $state<Record<string, string | undefined>>({}); let documentosStorage = $state<Record<string, string | undefined>>({});
let loading = $state(true); let loading = $state(true);
let filtro = $state<string>('todos'); // todos, enviados, pendentes let filtro = $state<'todos' | 'enviados' | 'pendentes'>('todos'); // todos, enviados, pendentes
async function load() { async function load() {
try { try {
@@ -27,7 +28,7 @@
// Carregar dados do funcionário // Carregar dados do funcionário
const data = await client.query(api.funcionarios.getById, { const data = await client.query(api.funcionarios.getById, {
id: funcionarioId as any id: funcionarioId
}); });
if (!data) { if (!data) {
@@ -39,8 +40,10 @@
// Mapear storage IDs dos documentos // Mapear storage IDs dos documentos
documentos.forEach((doc) => { documentos.forEach((doc) => {
if ((data as any)[doc.campo]) { const campo = doc.campo as keyof Doc<'funcionarios'>;
documentosStorage[doc.campo] = (data as any)[doc.campo]; const valor = data[campo];
if (typeof valor === 'string') {
documentosStorage[doc.campo] = valor;
} }
}); });
} catch (err) { } catch (err) {
@@ -63,13 +66,23 @@
body: file body: file
}); });
const { storageId } = await result.json(); const uploadResponse = await result.json();
if (
!uploadResponse ||
typeof uploadResponse !== 'object' ||
!('storageId' in uploadResponse) ||
typeof uploadResponse.storageId !== 'string'
) {
throw new Error('Resposta inválida ao fazer upload');
}
const storageId = uploadResponse.storageId;
// Atualizar documento no funcionário // Atualizar documento no funcionário
await client.mutation(api.documentos.updateDocumento, { await client.mutation(api.documentos.updateDocumento, {
funcionarioId: funcionarioId as any, funcionarioId,
campo, campo,
storageId: storageId as any storageId: storageId as Id<'_storage'>
}); });
// Atualizar localmente // Atualizar localmente
@@ -77,8 +90,11 @@
// Recarregar // Recarregar
await load(); await load();
} catch (err: any) { } catch (err) {
throw new Error(err?.message || 'Erro ao fazer upload'); if (err instanceof Error && err.message) {
throw err;
}
throw new Error('Erro ao fazer upload');
} }
} }
@@ -86,7 +102,7 @@
try { try {
// Atualizar documento no funcionário (set to null) // Atualizar documento no funcionário (set to null)
await client.mutation(api.documentos.updateDocumento, { await client.mutation(api.documentos.updateDocumento, {
funcionarioId: funcionarioId as any, funcionarioId,
campo, campo,
storageId: null storageId: null
}); });
@@ -96,8 +112,10 @@
// Recarregar // Recarregar
await load(); await load();
} catch (err: any) { } catch (err) {
alert('Erro ao remover documento: ' + (err?.message || '')); const mensagem =
err instanceof Error && err.message ? err.message : 'Erro ao remover documento';
alert('Erro ao remover documento: ' + mensagem);
} }
} }
@@ -130,7 +148,9 @@
<div class="breadcrumbs mb-4 text-sm"> <div class="breadcrumbs mb-4 text-sm">
<ul> <ul>
<li> <li>
<a href={resolve('/recursos-humanos')} class="text-primary hover:underline">Recursos Humanos</a> <a href={resolve('/recursos-humanos')} class="text-primary hover:underline"
>Recursos Humanos</a
>
</li> </li>
<li> <li>
<a href={resolve('/recursos-humanos/funcionarios')} class="text-primary hover:underline" <a href={resolve('/recursos-humanos/funcionarios')} class="text-primary hover:underline"
@@ -305,7 +325,7 @@
</div> </div>
<!-- Documentos por Categoria --> <!-- Documentos por Categoria -->
{#each categoriasDocumentos as categoria} {#each categoriasDocumentos as categoria (categoria)}
{@const docsCategoria = getDocumentosByCategoria(categoria).filter((doc) => { {@const docsCategoria = getDocumentosByCategoria(categoria).filter((doc) => {
const temDocumento = !!documentosStorage[doc.campo]; const temDocumento = !!documentosStorage[doc.campo];
if (filtro === 'enviados') return temDocumento; if (filtro === 'enviados') return temDocumento;
@@ -322,7 +342,7 @@
</h2> </h2>
<div class="grid grid-cols-1 gap-4 md:grid-cols-2"> <div class="grid grid-cols-1 gap-4 md:grid-cols-2">
{#each docsCategoria as doc} {#each docsCategoria as doc (doc.campo)}
<FileUpload <FileUpload
label={doc.nome} label={doc.nome}
helpUrl={doc.helpUrl} helpUrl={doc.helpUrl}

View File

@@ -483,7 +483,9 @@
<div class="breadcrumbs mb-4 text-sm"> <div class="breadcrumbs mb-4 text-sm">
<ul> <ul>
<li> <li>
<a href={resolve('/recursos-humanos')} class="text-primary hover:underline">Recursos Humanos</a> <a href={resolve('/recursos-humanos')} class="text-primary hover:underline"
>Recursos Humanos</a
>
</li> </li>
<li> <li>
<a href={resolve('/recursos-humanos/funcionarios')} class="text-primary hover:underline" <a href={resolve('/recursos-humanos/funcionarios')} class="text-primary hover:underline"