diff --git a/packages/admin/src/routes/_authenticated/settings.tsx b/packages/admin/src/routes/_authenticated/settings.tsx index 2da5826..ba08512 100644 --- a/packages/admin/src/routes/_authenticated/settings.tsx +++ b/packages/admin/src/routes/_authenticated/settings.tsx @@ -1,4 +1,4 @@ -import { useState } from 'react' +import { useState, useRef, useEffect } from 'react' import { createFileRoute } from '@tanstack/react-router' import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' import { queryOptions } from '@tanstack/react-query' @@ -134,17 +134,8 @@ function SettingsPage() { {/* Logo upload */} -
-
- - -

Used on PDFs, invoices, receipts

-
-
- - -

Sidebar & login screen

-
+
+
{editing ? ( @@ -388,3 +379,84 @@ function AddLocationDialog({ open, onOpenChange }: { open: boolean; onOpenChange ) } + +function LogoUpload({ entityId, category, label, description }: { entityId: string; category: string; label: string; description: string }) { + const queryClient = useQueryClient() + const token = useAuthStore((s) => s.token) + const fileInputRef = useRef(null) + const [uploading, setUploading] = useState(false) + const [imgSrc, setImgSrc] = useState(null) + + const { data: filesData } = useQuery(queryOptions({ + queryKey: ['files', 'company', entityId], + queryFn: () => api.get<{ data: { id: string; path: string; filename: string }[] }>('/v1/files', { entityType: 'company', entityId }), + enabled: !!entityId, + })) + + const logoFile = filesData?.data?.find((f) => f.path.includes(`/${category}-`)) + + // Load image via authenticated fetch + useEffect(() => { + if (!logoFile || !token) { setImgSrc(null); return } + let cancelled = false + let blobUrl: string | null = null + async function load() { + try { + const res = await fetch(`/v1/files/serve/${logoFile!.path}`, { + headers: { Authorization: `Bearer ${token}` }, + }) + if (!res.ok || cancelled) return + const blob = await res.blob() + if (!cancelled) { blobUrl = URL.createObjectURL(blob); setImgSrc(blobUrl) } + } catch { /* ignore */ } + } + load() + return () => { cancelled = true; if (blobUrl) URL.revokeObjectURL(blobUrl) } + }, [logoFile?.path, token]) + + async function handleUpload(e: React.ChangeEvent) { + const file = e.target.files?.[0] + if (!file) return + setUploading(true) + try { + if (logoFile) await api.del(`/v1/files/${logoFile.id}`) + const formData = new FormData() + formData.append('file', file) + formData.append('entityType', 'company') + formData.append('entityId', entityId) + formData.append('category', category) + const res = await fetch('/v1/files', { method: 'POST', headers: { Authorization: `Bearer ${token}` }, body: formData }) + if (!res.ok) throw new Error('Upload failed') + queryClient.invalidateQueries({ queryKey: ['files', 'company', entityId] }) + toast.success(`${label} updated`) + } catch (err) { + toast.error(err instanceof Error ? err.message : 'Upload failed') + } finally { + setUploading(false) + e.target.value = '' + } + } + + return ( +
+ + +

{description}

+ +
+ ) +} diff --git a/packages/backend/src/routes/v1/files.ts b/packages/backend/src/routes/v1/files.ts index f45f9b1..c8d8865 100644 --- a/packages/backend/src/routes/v1/files.ts +++ b/packages/backend/src/routes/v1/files.ts @@ -90,7 +90,7 @@ export const fileRoutes: FastifyPluginAsync = async (app) => { const data = await app.storage.get(filePath) const ext = filePath.split('.').pop()?.toLowerCase() const contentTypeMap: Record = { - jpg: 'image/jpeg', jpeg: 'image/jpeg', png: 'image/png', webp: 'image/webp', pdf: 'application/pdf', + jpg: 'image/jpeg', jpeg: 'image/jpeg', png: 'image/png', webp: 'image/webp', svg: 'image/svg+xml', pdf: 'application/pdf', } return reply .header('Content-Type', contentTypeMap[ext ?? ''] ?? 'application/octet-stream') @@ -154,7 +154,7 @@ export const fileRoutes: FastifyPluginAsync = async (app) => { const data = await app.storage.get(filePath) const ext = filePath.split('.').pop()?.toLowerCase() const contentTypeMap: Record = { - jpg: 'image/jpeg', jpeg: 'image/jpeg', png: 'image/png', webp: 'image/webp', pdf: 'application/pdf', + jpg: 'image/jpeg', jpeg: 'image/jpeg', png: 'image/png', webp: 'image/webp', svg: 'image/svg+xml', pdf: 'application/pdf', } return reply .header('Content-Type', contentTypeMap[ext ?? ''] ?? 'application/octet-stream')