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>;
} }
@@ -48,38 +48,33 @@
"image/png", "image/png",
]; ];
// Buscar URL do arquivo quando houver um storageId // Buscar URL do arquivo quando houver um storageId
$effect(() => { $effect(() => {
if (value && !fileName) { if (value && !fileName) {
// Tem storageId mas não é um upload recente // Tem storageId mas não é um upload recente
loadExistingFile(value); loadExistingFile(value);
} }
}); });
async function loadExistingFile(storageId: string) {
try {
const url = await client.storage.getUrl(storageId as any);
if (url) {
fileUrl = url;
fileName = "Documento anexado";
// Detectar tipo pelo URL ou assumir PDF
if (url.includes(".pdf") || url.includes("application/pdf")) {
fileType = "application/pdf";
} else {
fileType = "image/jpeg";
previewUrl = url; // Para imagens, a URL serve como preview
}
}
} catch (err) {
console.error("Erro ao carregar arquivo existente:", err);
}
}
async function loadExistingFile(storageId: string) {
try {
const url = await client.storage.getUrl(storageId as any);
if (url) {
async function handleFileSelect(event: Event) { async function handleFileSelect(event: Event) {
const target = event.target as HTMLInputElement; const target = event.target as HTMLInputElement;
const file = target.files?.[0]; const file = target.files?.[0];
// Detectar tipo pelo URL ou assumir PDF
if (!file) return; if (url.includes('.pdf') || url.includes('application/pdf')) {
fileType = 'application/pdf';
} else {
fileType = 'image/jpeg';
previewUrl = url; // Para imagens, a URL serve como preview
}
}
} catch (err) {
console.error('Erro ao carregar arquivo existente:', err);
}
}
error = null; error = null;
@@ -187,32 +182,22 @@
{disabled} {disabled}
/> />
{#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 -->
> <div class="shrink-0">
<!-- Preview --> {#if previewUrl}
<div class="shrink-0"> <img src={previewUrl} alt="Preview" class="h-12 w-12 rounded object-cover" />
{#if previewUrl} {:else if fileType === 'application/pdf' || fileName.endsWith('.pdf')}
<img <div class="bg-error/10 flex h-12 w-12 items-center justify-center rounded">
src={previewUrl} <FileText class="text-error h-6 w-6" strokeWidth={2} />
alt="Preview" </div>
class="w-12 h-12 object-cover rounded" {:else}
/> <div class="bg-success/10 flex h-12 w-12 items-center justify-center rounded">
{:else if fileType === "application/pdf" || fileName.endsWith(".pdf")} <File class="text-success h-6 w-6" strokeWidth={2} />
<div </div>
class="w-12 h-12 bg-error/10 rounded flex items-center justify-center" {/if}
> </div>
<FileText class="h-6 w-6 text-error" strokeWidth={2} />
</div>
{:else}
<div
class="w-12 h-12 bg-success/10 rounded flex items-center justify-center"
>
<File class="h-6 w-6 text-success" strokeWidth={2} />
</div>
{/if}
</div>
<!-- File info --> <!-- File info -->
<div class="flex-1 min-w-0"> <div class="flex-1 min-w-0">

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"