diff --git a/frontend/pages/settings/tenant.vue b/frontend/pages/settings/tenant.vue
index d6dc10fd..b620b7a5 100644
--- a/frontend/pages/settings/tenant.vue
+++ b/frontend/pages/settings/tenant.vue
@@ -3,6 +3,7 @@ import {
TAX_EVALUATION_PERIOD_OPTIONS,
normalizeTaxEvaluationPeriod
} from "~/composables/useTaxEvaluation"
+import { DEFAULT_PORTAL_COLOR, getPortalColor, normalizePortalColor, portalThemeStyle } from "~/utils/portalTheme"
const auth = useAuthStore()
const toast = useToast()
@@ -260,6 +261,16 @@ const setupPage = async () => {
const features = ref({ ...defaultFeatures, ...(auth.activeTenantData?.features || {}) })
const businessInfo = ref(normalizeBusinessInfo(auth.activeTenantData?.businessInfo))
+const portalColor = ref(getPortalColor(auth.activeTenantData?.portalConfig))
+const portalColorSaving = ref(false)
+const validPortalColor = computed(() => normalizePortalColor(portalColor.value))
+const portalPreviewStyle = computed(() => portalThemeStyle(portalColor.value))
+watch(() => auth.activeTenant, () => {
+ portalColor.value = getPortalColor(auth.activeTenantData?.portalConfig)
+})
+watch(() => auth.activeTenantData?.portalConfig, (config) => {
+ portalColor.value = getPortalColor(config)
+})
const accountChart = ref(auth.activeTenantData.accountChart || "skr03")
const taxEvaluationPeriod = ref(normalizeTaxEvaluationPeriod(auth.activeTenantData.taxEvaluationPeriod))
const accountChartOptions = [
@@ -288,6 +299,18 @@ const saveFeatures = async () => {
await updateTenant({features: features.value})
}
+const savePortalColor = async () => {
+ if (!validPortalColor.value || portalColorSaving.value) return
+ portalColorSaving.value = true
+ try {
+ const { primayColor, ...portalConfig } = auth.activeTenantData?.portalConfig || {}
+ await updateTenant({ portalConfig: { ...portalConfig, primaryColor: validPortalColor.value } })
+ toast.add({ title: "Akzentfarbe für das Kundenportal gespeichert", color: "success" })
+ } finally {
+ portalColorSaving.value = false
+ }
+}
+
const formatMcpTokenDate = (value) => {
if (!value) return "Nie"
@@ -614,6 +637,8 @@ onMounted(() => {
label: 'Dokubox'
},{
label: 'Rechnung & Kontakt'
+ },{
+ label: 'Kundenportal'
},{
label: 'Plantafel'
},{
@@ -755,6 +780,43 @@ onMounted(() => {
+
+
+
+
+
Akzentfarbe im Kundenportal
+
Wähle die Farbe für Schaltflächen, Reiter und Hervorhebungen im Kundenportal deiner Firma.
+
+
+
+
+
+
+
+
+
Vorschau Kundenportal
+
+ Kundendaten speichern
+ Rechnung
+
+
+
+ Akzentfarbe speichern
+ FEDEO-Grün verwenden
+
+
+
+
diff --git a/frontend/utils/portalTheme.ts b/frontend/utils/portalTheme.ts
new file mode 100644
index 00000000..802fa6a8
--- /dev/null
+++ b/frontend/utils/portalTheme.ts
@@ -0,0 +1,32 @@
+export const DEFAULT_PORTAL_COLOR = "#69c350"
+
+export function normalizePortalColor(value: unknown): string | null {
+ if (typeof value !== "string") return null
+ const color = value.trim().toLowerCase()
+ return /^#[0-9a-f]{6}$/.test(color) ? color : null
+}
+
+export function getPortalColor(config: any): string {
+ // Older tenant records contain the misspelled key from the original schema.
+ return normalizePortalColor(config?.primaryColor)
+ || normalizePortalColor(config?.primayColor)
+ || DEFAULT_PORTAL_COLOR
+}
+
+export function portalThemeStyle(value: unknown): Record {
+ const color = normalizePortalColor(value) || DEFAULT_PORTAL_COLOR
+ const rgb = [1, 3, 5].map(offset => parseInt(color.slice(offset, offset + 2), 16) / 255)
+ const linear = rgb.map(channel => channel <= 0.04045 ? channel / 12.92 : ((channel + 0.055) / 1.055) ** 2.4)
+ const luminance = linear[0] * 0.2126 + linear[1] * 0.7152 + linear[2] * 0.0722
+ const style: Record = {
+ "--ui-primary": color,
+ "--portal-accent-contrast": luminance > 0.179 ? "#000000" : "#ffffff",
+ }
+ const shades = { 50: 5, 100: 10, 200: 25, 300: 45, 400: 70, 500: 100, 600: 80, 700: 65, 800: 50, 900: 35, 950: 20 }
+ for (const [shade, percent] of Object.entries(shades)) {
+ style[`--ui-color-primary-${shade}`] = Number(shade) === 500
+ ? color
+ : `color-mix(in srgb, ${color} ${percent}%, ${Number(shade) < 500 ? "white" : "black"})`
+ }
+ return style
+}