diff --git a/src/api/index.ts b/src/api/index.ts index 4fcb99c..6cbfe06 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -19,11 +19,10 @@ class ApiClient { private async request( method: string, endpoint: string, - data?: unknown + data?: unknown, + options?: { headers?: Record } ): Promise> { - const headers: Record = { - 'Content-Type': 'application/json', - } + const headers: Record = {} const token = this.getToken() if (token) { @@ -35,8 +34,24 @@ class ApiClient { headers, } - if (data) { + // Handle FormData (multipart/form-data) vs JSON + if (data instanceof FormData) { + config.body = data + // Don't set Content-Type for FormData - browser will set it with boundary + } else if (data) { + headers['Content-Type'] = 'application/json' config.body = JSON.stringify(data) + } else { + headers['Content-Type'] = 'application/json' + } + + // Merge custom headers + if (options?.headers) { + Object.assign(headers, options.headers) + // Remove Content-Type if it's multipart (let browser handle it) + if (options.headers['Content-Type']?.includes('multipart')) { + delete headers['Content-Type'] + } } const response = await fetch(`${this.baseUrl}${endpoint}`, config) @@ -90,8 +105,8 @@ class ApiClient { return this.request('GET', endpoint) } - post(endpoint: string, data?: unknown): Promise> { - return this.request('POST', endpoint, data) + post(endpoint: string, data?: unknown, options?: { headers?: Record }): Promise> { + return this.request('POST', endpoint, data, options) } put(endpoint: string, data?: unknown): Promise> { diff --git a/src/components/layout/AppSidebar.vue b/src/components/layout/AppSidebar.vue index 56268a5..a04b92b 100644 --- a/src/components/layout/AppSidebar.vue +++ b/src/components/layout/AppSidebar.vue @@ -76,7 +76,16 @@ function isActive(href: string) { >
- 🔐 SeCu + +
diff --git a/src/stores/auth.ts b/src/stores/auth.ts index 01d356a..8a6e02e 100644 --- a/src/stores/auth.ts +++ b/src/stores/auth.ts @@ -11,8 +11,17 @@ export interface User { org_id: string } +export interface Organization { + id: string + name: string + slug: string + logo_url: string | null + settings?: Record +} + export const useAuthStore = defineStore('auth', () => { const user = ref(null) + const organization = ref(null) const accessToken = ref(localStorage.getItem('accessToken')) const refreshToken = ref(localStorage.getItem('refreshToken')) const orgSlug = ref(localStorage.getItem('orgSlug') || '') @@ -25,6 +34,8 @@ export const useAuthStore = defineStore('auth', () => { const canManageUsers = computed(() => isChef.value || isDisponent.value) const canManageOrders = computed(() => isChef.value || isDisponent.value) const fullName = computed(() => user.value ? `${user.value.first_name} ${user.value.last_name}` : '') + const orgName = computed(() => organization.value?.name || '') + const orgLogo = computed(() => organization.value?.logo_url || null) function setTokens(access: string, refresh: string) { accessToken.value = access @@ -40,6 +51,7 @@ export const useAuthStore = defineStore('auth', () => { function clearAuth() { user.value = null + organization.value = null accessToken.value = null refreshToken.value = null localStorage.removeItem('accessToken') @@ -85,11 +97,28 @@ export const useAuthStore = defineStore('auth', () => { try { const response = await api.get('/auth/me') user.value = response.data.user + // Also fetch organization info + await fetchOrganization() } catch { clearAuth() } } + async function fetchOrganization() { + try { + const response = await api.get('/organizations/current') + organization.value = response.data.organization + } catch { + // Ignore errors - org info is optional + } + } + + function setOrgLogo(logoUrl: string | null) { + if (organization.value) { + organization.value.logo_url = logoUrl + } + } + async function refreshAccessToken() { if (!refreshToken.value) { throw new Error('No refresh token') @@ -107,6 +136,7 @@ export const useAuthStore = defineStore('auth', () => { return { user, + organization, accessToken, refreshToken, orgSlug, @@ -118,11 +148,15 @@ export const useAuthStore = defineStore('auth', () => { canManageUsers, canManageOrders, fullName, + orgName, + orgLogo, login, register, logout, fetchCurrentUser, + fetchOrganization, refreshAccessToken, - setOrgSlug + setOrgSlug, + setOrgLogo } }) diff --git a/src/views/SettingsView.vue b/src/views/SettingsView.vue index 9628400..1407a6d 100644 --- a/src/views/SettingsView.vue +++ b/src/views/SettingsView.vue @@ -14,6 +14,14 @@ const message = ref('') const error = ref('') const showSecuritySettings = ref(false) +// Logo upload +const logoFile = ref(null) +const logoPreview = ref(null) +const logoLoading = ref(false) +const logoMessage = ref('') +const logoError = ref('') +const fileInput = ref(null) + // App lock status const lockMethod = ref(null) @@ -23,8 +31,91 @@ onMounted(() => { if (authStore.user?.email) { localStorage.setItem('userEmail', authStore.user.email) } + // Set logo preview from current org logo + if (authStore.orgLogo) { + logoPreview.value = authStore.orgLogo + } }) +function handleLogoSelect(event: Event) { + const target = event.target as HTMLInputElement + const file = target.files?.[0] + + if (!file) return + + // Validate file type + const allowedTypes = ['image/jpeg', 'image/png', 'image/webp', 'image/svg+xml'] + if (!allowedTypes.includes(file.type)) { + logoError.value = 'Nur JPG, PNG, WebP oder SVG erlaubt' + return + } + + // Validate file size (5MB) + if (file.size > 5 * 1024 * 1024) { + logoError.value = 'Datei zu groß (max 5MB)' + return + } + + logoFile.value = file + logoError.value = '' + + // Create preview + const reader = new FileReader() + reader.onload = (e) => { + logoPreview.value = e.target?.result as string + } + reader.readAsDataURL(file) +} + +async function uploadLogo() { + if (!logoFile.value) return + + logoLoading.value = true + logoError.value = '' + logoMessage.value = '' + + try { + const formData = new FormData() + formData.append('logo', logoFile.value) + + const response = await api.post('/uploads/logo', formData, { + headers: { + 'Content-Type': 'multipart/form-data' + } + }) + + logoMessage.value = 'Logo erfolgreich hochgeladen' + logoFile.value = null + + // Update store with new logo URL + authStore.setOrgLogo(response.data.logo_url) + logoPreview.value = response.data.logo_url + } catch (e) { + logoError.value = e instanceof Error ? e.message : 'Fehler beim Hochladen' + } finally { + logoLoading.value = false + } +} + +async function deleteLogo() { + if (!confirm('Logo wirklich löschen?')) return + + logoLoading.value = true + logoError.value = '' + + try { + await api.delete('/uploads/logo') + logoMessage.value = 'Logo gelöscht' + logoPreview.value = null + logoFile.value = null + authStore.setOrgLogo(null) + } catch (e) { + logoError.value = e instanceof Error ? e.message : 'Fehler beim Löschen' + } finally { + logoLoading.value = false + } +} + const lockStatusText = computed(() => { if (!lockMethod.value || lockMethod.value === 'none') return 'Deaktiviert' if (lockMethod.value === 'biometric') return 'Fingerabdruck / Face ID' @@ -96,6 +187,84 @@ function handleSecurityClose() {

{{ authStore.user?.role }}

+
+ +

{{ authStore.orgName }}

+
+ + + + +
+

🏢 Firmenlogo

+

+ Lade ein Logo für deine Organisation hoch. Deine Mitarbeiter sehen es in der Sidebar. +

+ +
+ +
+
+ Logo Vorschau + 🏢 +
+
+ + +
+
+ + + + {{ logoFile.name }} + +
+ +

+ JPG, PNG, WebP oder SVG • max. 5MB • empfohlen: 200x80px +

+ +
+ + +
+ +
{{ logoError }}
+
{{ logoMessage }}
+