fix(email-signature): correct addresses, add Albac, fix logo sizing, update US/SDT colors from logos, fix hydration error

- Fix all 3 address constants: Christescu (nr. 12, 400416), Unirii (nr. 3 sc. 3 ap. 26, 400432), Albac (nr. 2 ap. 1, 400459)
- Add 3rd address option (Albac) to all company address selectors
- Default address changed to Christescu for all companies
- Update US brand colors to logo blue (#345476), SDT to logo teal (#0182A1)
- Fix slashAccent for US/SDT (was pointing to logo files instead of slash assets)
- Add logoDimensions to CompanyBranding type for per-company logo sizing
- Set US logo to 140x24 and SDT to 71x24 (matching SVG aspect ratios)
- Fix sidebar hydration error: remove unused useTheme() hook call
- Update color palettes in configurator to match logo-derived colors

Tasks: 1.01 (verified), 1.02 (address toggle + fixes)
This commit is contained in:
AI Assistant
2026-02-18 23:09:10 +02:00
parent 5330ea536b
commit 42260a17a4
8 changed files with 548 additions and 315 deletions

View File

@@ -1,18 +1,38 @@
'use client';
"use client";
import type { CompanyId } from '@/core/auth/types';
import type { SignatureConfig, SignatureColors, SignatureLayout, SignatureVariant } from '../types';
import { COMPANY_BRANDING, BELETAGE_ADDRESSES, US_ADDRESSES, SDT_ADDRESSES } from '../services/company-branding';
import { Input } from '@/shared/components/ui/input';
import { Label } from '@/shared/components/ui/label';
import { Switch } from '@/shared/components/ui/switch';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/shared/components/ui/select';
import { Separator } from '@/shared/components/ui/separator';
import { cn } from '@/shared/lib/utils';
import type { CompanyId } from "@/core/auth/types";
import type {
SignatureConfig,
SignatureColors,
SignatureLayout,
SignatureVariant,
} from "../types";
import {
COMPANY_BRANDING,
BELETAGE_ADDRESSES,
US_ADDRESSES,
SDT_ADDRESSES,
} from "../services/company-branding";
import type { AddressKey } from "../services/company-branding";
import { Input } from "@/shared/components/ui/input";
import { Label } from "@/shared/components/ui/label";
import { Switch } from "@/shared/components/ui/switch";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/shared/components/ui/select";
import { Separator } from "@/shared/components/ui/separator";
import { cn } from "@/shared/lib/utils";
interface SignatureConfiguratorProps {
config: SignatureConfig;
onUpdateField: <K extends keyof SignatureConfig>(key: K, value: SignatureConfig[K]) => void;
onUpdateField: <K extends keyof SignatureConfig>(
key: K,
value: SignatureConfig[K],
) => void;
onUpdateColor: (key: keyof SignatureColors, value: string) => void;
onUpdateLayout: (key: keyof SignatureLayout, value: number) => void;
onSetVariant: (variant: SignatureVariant) => void;
@@ -23,58 +43,71 @@ interface SignatureConfiguratorProps {
/** Color palette per company */
const COMPANY_PALETTES: Record<CompanyId, Record<string, string>> = {
beletage: {
verde: '#22B5AB',
griInchis: '#54504F',
griDeschis: '#A7A9AA',
negru: '#323232',
verde: "#22B5AB",
griInchis: "#54504F",
griDeschis: "#A7A9AA",
negru: "#323232",
},
'urban-switch': {
indigo: '#6366f1',
violet: '#4F46E5',
griInchis: '#2D2D2D',
griDeschis: '#6B7280',
albastru: '#3B82F6',
negru: '#1F2937',
"urban-switch": {
albastru: "#345476",
griInchis: "#2D2D2D",
griDeschis: "#6B7280",
negru: "#1F2937",
},
'studii-de-teren': {
amber: '#f59e0b',
portocaliu: '#D97706',
griInchis: '#2D2D2D',
griDeschis: '#6B7280',
maro: '#92400E',
negru: '#1F2937',
"studii-de-teren": {
teal: "#0182A1",
bleumarin: "#000D1A",
griInchis: "#2D2D2D",
griDeschis: "#6B7280",
negru: "#1F2937",
},
group: {
gri: '#64748b',
griInchis: '#334155',
griDeschis: '#94a3b8',
negru: '#1e293b',
gri: "#64748b",
griInchis: "#334155",
griDeschis: "#94a3b8",
negru: "#1e293b",
},
};
const COLOR_LABELS: Record<keyof SignatureColors, string> = {
prefix: 'Titulatură',
name: 'Nume',
title: 'Funcție',
address: 'Adresă',
phone: 'Telefon',
website: 'Website',
motto: 'Motto',
prefix: "Titulatură",
name: "Nume",
title: "Funcție",
address: "Adresă",
phone: "Telefon",
website: "Website",
motto: "Motto",
};
const LAYOUT_CONTROLS: { key: keyof SignatureLayout; label: string; min: number; max: number }[] = [
{ key: 'greenLineWidth', label: 'Lungime linie accent', min: 50, max: 300 },
{ key: 'sectionSpacing', label: 'Spațiere secțiuni', min: 0, max: 30 },
{ key: 'logoSpacing', label: 'Spațiere logo', min: 0, max: 30 },
{ key: 'titleSpacing', label: 'Spațiere funcție', min: 0, max: 20 },
{ key: 'gutterWidth', label: 'Aliniere contact', min: 0, max: 150 },
{ key: 'iconTextSpacing', label: 'Spațiu icon-text', min: -10, max: 30 },
{ key: 'iconVerticalOffset', label: 'Aliniere verticală iconițe', min: -10, max: 10 },
{ key: 'mottoSpacing', label: 'Spațiere motto', min: 0, max: 20 },
const LAYOUT_CONTROLS: {
key: keyof SignatureLayout;
label: string;
min: number;
max: number;
}[] = [
{ key: "greenLineWidth", label: "Lungime linie accent", min: 50, max: 300 },
{ key: "sectionSpacing", label: "Spațiere secțiuni", min: 0, max: 30 },
{ key: "logoSpacing", label: "Spațiere logo", min: 0, max: 30 },
{ key: "titleSpacing", label: "Spațiere funcție", min: 0, max: 20 },
{ key: "gutterWidth", label: "Aliniere contact", min: 0, max: 150 },
{ key: "iconTextSpacing", label: "Spațiu icon-text", min: -10, max: 30 },
{
key: "iconVerticalOffset",
label: "Aliniere verticală iconițe",
min: -10,
max: 10,
},
{ key: "mottoSpacing", label: "Spațiere motto", min: 0, max: 20 },
];
export function SignatureConfigurator({
config, onUpdateField, onUpdateColor, onUpdateLayout, onSetVariant, onSetCompany, onSetAddress,
config,
onUpdateField,
onUpdateColor,
onUpdateLayout,
onSetVariant,
onSetCompany,
onSetAddress,
}: SignatureConfiguratorProps) {
const palette = COMPANY_PALETTES[config.company];
@@ -83,71 +116,129 @@ export function SignatureConfigurator({
{/* Company selector */}
<div>
<Label>Companie</Label>
<Select value={config.company} onValueChange={(v) => onSetCompany(v as CompanyId)}>
<Select
value={config.company}
onValueChange={(v) => onSetCompany(v as CompanyId)}
>
<SelectTrigger className="mt-1">
<SelectValue />
</SelectTrigger>
<SelectContent>
{Object.values(COMPANY_BRANDING).map((b) => (
<SelectItem key={b.id} value={b.id}>{b.name}</SelectItem>
<SelectItem key={b.id} value={b.id}>
{b.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{/* Address selector (for Beletage) */}
{config.company === 'beletage' && onSetAddress && (
{config.company === "beletage" && onSetAddress && (
<div>
<Label>Adresă birou</Label>
<Select
value={!config.addressOverride || BELETAGE_ADDRESSES.unirii.join('|') === config.addressOverride.join('|') ? 'unirii' : 'christescu'}
value={
!config.addressOverride
? "christescu"
: config.addressOverride.join("|") ===
BELETAGE_ADDRESSES.unirii.join("|")
? "unirii"
: config.addressOverride.join("|") ===
BELETAGE_ADDRESSES.albac.join("|")
? "albac"
: "christescu"
}
onValueChange={(v) => {
const key = v as keyof typeof BELETAGE_ADDRESSES;
const key = v as AddressKey;
onSetAddress(BELETAGE_ADDRESSES[key]);
}}
>
<SelectTrigger className="mt-1"><SelectValue /></SelectTrigger>
<SelectTrigger className="mt-1">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="unirii">Str. Unirii, nr. 3</SelectItem>
<SelectItem value="christescu">Str. G-ral Eremia Grigorescu, nr. 21</SelectItem>
<SelectItem value="christescu">
Str. G-ral Constantin Christescu, nr. 12
</SelectItem>
<SelectItem value="unirii">
Str. Unirii, nr. 3, sc. 3 ap. 26
</SelectItem>
<SelectItem value="albac">Str. Albac, nr. 2, ap. 1</SelectItem>
</SelectContent>
</Select>
</div>
)}
{/* Address selector (for Urban Switch) */}
{config.company === 'urban-switch' && onSetAddress && (
{config.company === "urban-switch" && onSetAddress && (
<div>
<Label>Adresă birou</Label>
<Select
value={!config.addressOverride || US_ADDRESSES.unirii.join('|') === config.addressOverride.join('|') ? 'unirii' : 'unirii'}
value={
!config.addressOverride
? "christescu"
: config.addressOverride.join("|") ===
US_ADDRESSES.unirii.join("|")
? "unirii"
: config.addressOverride.join("|") ===
US_ADDRESSES.albac.join("|")
? "albac"
: "christescu"
}
onValueChange={(v) => {
const key = v as keyof typeof US_ADDRESSES;
const key = v as AddressKey;
onSetAddress(US_ADDRESSES[key]);
}}
>
<SelectTrigger className="mt-1"><SelectValue /></SelectTrigger>
<SelectTrigger className="mt-1">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="unirii">Str. Unirii, nr. 3</SelectItem>
<SelectItem value="christescu">
Str. G-ral Constantin Christescu, nr. 12
</SelectItem>
<SelectItem value="unirii">
Str. Unirii, nr. 3, sc. 3 ap. 26
</SelectItem>
<SelectItem value="albac">Str. Albac, nr. 2, ap. 1</SelectItem>
</SelectContent>
</Select>
</div>
)}
{/* Address selector (for Studii de Teren) */}
{config.company === 'studii-de-teren' && onSetAddress && (
{config.company === "studii-de-teren" && onSetAddress && (
<div>
<Label>Adresă birou</Label>
<Select
value={!config.addressOverride || SDT_ADDRESSES.unirii.join('|') === config.addressOverride.join('|') ? 'unirii' : 'unirii'}
value={
!config.addressOverride
? "christescu"
: config.addressOverride.join("|") ===
SDT_ADDRESSES.unirii.join("|")
? "unirii"
: config.addressOverride.join("|") ===
SDT_ADDRESSES.albac.join("|")
? "albac"
: "christescu"
}
onValueChange={(v) => {
const key = v as keyof typeof SDT_ADDRESSES;
const key = v as AddressKey;
onSetAddress(SDT_ADDRESSES[key]);
}}
>
<SelectTrigger className="mt-1"><SelectValue /></SelectTrigger>
<SelectTrigger className="mt-1">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="unirii">Str. Unirii, nr. 3</SelectItem>
<SelectItem value="christescu">
Str. G-ral Constantin Christescu, nr. 12
</SelectItem>
<SelectItem value="unirii">
Str. Unirii, nr. 3, sc. 3 ap. 26
</SelectItem>
<SelectItem value="albac">Str. Albac, nr. 2, ap. 1</SelectItem>
</SelectContent>
</Select>
</div>
@@ -160,19 +251,40 @@ export function SignatureConfigurator({
<h3 className="text-sm font-semibold">Date personale</h3>
<div>
<Label htmlFor="sig-prefix">Titulatură (prefix)</Label>
<Input id="sig-prefix" value={config.prefix} onChange={(e) => onUpdateField('prefix', e.target.value)} className="mt-1" />
<Input
id="sig-prefix"
value={config.prefix}
onChange={(e) => onUpdateField("prefix", e.target.value)}
className="mt-1"
/>
</div>
<div>
<Label htmlFor="sig-name">Nume și Prenume</Label>
<Input id="sig-name" value={config.name} onChange={(e) => onUpdateField('name', e.target.value)} className="mt-1" />
<Input
id="sig-name"
value={config.name}
onChange={(e) => onUpdateField("name", e.target.value)}
className="mt-1"
/>
</div>
<div>
<Label htmlFor="sig-title">Funcția</Label>
<Input id="sig-title" value={config.title} onChange={(e) => onUpdateField('title', e.target.value)} className="mt-1" />
<Input
id="sig-title"
value={config.title}
onChange={(e) => onUpdateField("title", e.target.value)}
className="mt-1"
/>
</div>
<div>
<Label htmlFor="sig-phone">Telefon (format 07xxxxxxxx)</Label>
<Input id="sig-phone" type="tel" value={config.phone} onChange={(e) => onUpdateField('phone', e.target.value)} className="mt-1" />
<Input
id="sig-phone"
type="tel"
value={config.phone}
onChange={(e) => onUpdateField("phone", e.target.value)}
className="mt-1"
/>
</div>
</div>
@@ -181,19 +293,32 @@ export function SignatureConfigurator({
{/* Variant */}
<div className="space-y-3">
<h3 className="text-sm font-semibold">Variantă</h3>
<Select value={config.variant} onValueChange={(v) => onSetVariant(v as SignatureVariant)}>
<Select
value={config.variant}
onValueChange={(v) => onSetVariant(v as SignatureVariant)}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="full">Completă (logo + adresă + motto)</SelectItem>
<SelectItem value="full">
Completă (logo + adresă + motto)
</SelectItem>
<SelectItem value="reply">Simplă (fără logo/adresă)</SelectItem>
<SelectItem value="minimal">Super-simplă (doar nume/telefon)</SelectItem>
<SelectItem value="minimal">
Super-simplă (doar nume/telefon)
</SelectItem>
</SelectContent>
</Select>
<div className="flex items-center gap-2">
<Switch checked={config.useSvg} onCheckedChange={(v) => onUpdateField('useSvg', v)} id="svg-toggle" />
<Label htmlFor="svg-toggle" className="cursor-pointer text-sm">Imagini SVG (calitate maximă)</Label>
<Switch
checked={config.useSvg}
onCheckedChange={(v) => onUpdateField("useSvg", v)}
id="svg-toggle"
/>
<Label htmlFor="svg-toggle" className="cursor-pointer text-sm">
Imagini SVG (calitate maximă)
</Label>
</div>
</div>
@@ -202,27 +327,31 @@ export function SignatureConfigurator({
{/* Colors — company-specific palette */}
<div className="space-y-3">
<h3 className="text-sm font-semibold">Culori text</h3>
{(Object.keys(COLOR_LABELS) as (keyof SignatureColors)[]).map((colorKey) => (
<div key={colorKey} className="flex items-center justify-between">
<span className="text-sm text-muted-foreground">{COLOR_LABELS[colorKey]}</span>
<div className="flex gap-1.5">
{Object.values(palette).map((color) => (
<button
key={color}
type="button"
onClick={() => onUpdateColor(colorKey, color)}
className={cn(
'h-6 w-6 rounded-full border-2 transition-all',
config.colors[colorKey] === color
? 'border-primary scale-110 ring-2 ring-primary/30'
: 'border-transparent hover:scale-105'
)}
style={{ backgroundColor: color }}
/>
))}
{(Object.keys(COLOR_LABELS) as (keyof SignatureColors)[]).map(
(colorKey) => (
<div key={colorKey} className="flex items-center justify-between">
<span className="text-sm text-muted-foreground">
{COLOR_LABELS[colorKey]}
</span>
<div className="flex gap-1.5">
{Object.values(palette).map((color) => (
<button
key={color}
type="button"
onClick={() => onUpdateColor(colorKey, color)}
className={cn(
"h-6 w-6 rounded-full border-2 transition-all",
config.colors[colorKey] === color
? "border-primary scale-110 ring-2 ring-primary/30"
: "border-transparent hover:scale-105",
)}
style={{ backgroundColor: color }}
/>
))}
</div>
</div>
</div>
))}
),
)}
</div>
<Separator />
@@ -234,14 +363,18 @@ export function SignatureConfigurator({
<div key={key}>
<div className="flex justify-between text-sm">
<Label>{label}</Label>
<span className="text-muted-foreground">{config.layout[key]}px</span>
<span className="text-muted-foreground">
{config.layout[key]}px
</span>
</div>
<input
type="range"
min={min}
max={max}
value={config.layout[key]}
onChange={(e) => onUpdateLayout(key, parseInt(e.target.value, 10))}
onChange={(e) =>
onUpdateLayout(key, parseInt(e.target.value, 10))
}
className="mt-1 w-full accent-primary"
/>
</div>

View File

@@ -1,132 +1,150 @@
import type { CompanyId } from '@/core/auth/types';
import type { CompanyBranding, SignatureColors } from '../types';
import type { CompanyId } from "@/core/auth/types";
import type { CompanyBranding, SignatureColors } from "../types";
const BELETAGE_COLORS: SignatureColors = {
prefix: '#54504F',
name: '#54504F',
title: '#A7A9AA',
address: '#A7A9AA',
phone: '#54504F',
website: '#54504F',
motto: '#22B5AB',
prefix: "#54504F",
name: "#54504F",
title: "#A7A9AA",
address: "#A7A9AA",
phone: "#54504F",
website: "#54504F",
motto: "#22B5AB",
};
const URBAN_SWITCH_COLORS: SignatureColors = {
prefix: '#2D2D2D',
name: '#2D2D2D',
title: '#6B7280',
address: '#6B7280',
phone: '#2D2D2D',
website: '#4F46E5',
motto: '#6366f1',
prefix: "#345476",
name: "#345476",
title: "#6B7280",
address: "#6B7280",
phone: "#345476",
website: "#345476",
motto: "#345476",
};
const STUDII_COLORS: SignatureColors = {
prefix: '#2D2D2D',
name: '#2D2D2D',
title: '#6B7280',
address: '#6B7280',
phone: '#2D2D2D',
website: '#D97706',
motto: '#f59e0b',
prefix: "#000D1A",
name: "#000D1A",
title: "#6B7280",
address: "#6B7280",
phone: "#000D1A",
website: "#0182A1",
motto: "#0182A1",
};
const ADDR_UNIRII = ['str. Unirii, nr. 3, ap. 26', 'Cluj-Napoca, Cluj 400417', 'România'] as const;
const ADDR_CHRISTESCU = ['str. G-ral Eremia Grigorescu, nr. 21', 'Cluj-Napoca, Cluj 400304', 'România'] as const;
const ADDR_CHRISTESCU = [
"str. G-ral Constantin Christescu, nr. 12",
"Cluj-Napoca, Cluj 400416",
"România",
] as const;
const ADDR_UNIRII = [
"str. Unirii, nr. 3, sc. 3 ap. 26",
"Cluj-Napoca, Cluj 400432",
"România",
] as const;
const ADDR_ALBAC = [
"Str. Albac, nr. 2, ap. 1",
"Cluj-Napoca, Cluj 400459",
"România",
] as const;
/** Address option keys shared across all companies */
export type AddressKey = "christescu" | "unirii" | "albac";
/** Available address options for Beletage (toggle between offices) */
export const BELETAGE_ADDRESSES: { unirii: string[]; christescu: string[] } = {
unirii: [...ADDR_UNIRII],
export const BELETAGE_ADDRESSES: Record<AddressKey, string[]> = {
christescu: [...ADDR_CHRISTESCU],
unirii: [...ADDR_UNIRII],
albac: [...ADDR_ALBAC],
};
/** Available address options for Urban Switch */
export const US_ADDRESSES: { unirii: string[] } = {
unirii: ['str. Unirii, nr. 3, ap. 26', 'Cluj-Napoca, Cluj 400417', 'România'],
export const US_ADDRESSES: Record<AddressKey, string[]> = {
christescu: [...ADDR_CHRISTESCU],
unirii: [...ADDR_UNIRII],
albac: [...ADDR_ALBAC],
};
/** Available address options for Studii de Teren */
export const SDT_ADDRESSES: { unirii: string[] } = {
unirii: ['str. Unirii, nr. 3, ap. 26', 'Cluj-Napoca, Cluj 400417', 'românia'],
export const SDT_ADDRESSES: Record<AddressKey, string[]> = {
christescu: [...ADDR_CHRISTESCU],
unirii: [...ADDR_UNIRII],
albac: [...ADDR_ALBAC],
};
export const COMPANY_BRANDING: Record<CompanyId, CompanyBranding> = {
beletage: {
id: 'beletage',
name: 'Beletage SRL',
accent: '#22B5AB',
id: "beletage",
name: "Beletage SRL",
accent: "#22B5AB",
logo: {
png: 'https://beletage.ro/img/Semnatura-Logo.png',
svg: 'https://beletage.ro/img/Logo-Beletage.svg',
png: "https://beletage.ro/img/Semnatura-Logo.png",
svg: "https://beletage.ro/img/Logo-Beletage.svg",
},
slashGrey: {
png: 'https://beletage.ro/img/Grey-slash.png',
svg: 'https://beletage.ro/img/Grey-slash.svg',
png: "https://beletage.ro/img/Grey-slash.png",
svg: "https://beletage.ro/img/Grey-slash.svg",
},
slashAccent: {
png: 'https://beletage.ro/img/Green-slash.png',
svg: 'https://beletage.ro/img/Green-slash.svg',
png: "https://beletage.ro/img/Green-slash.png",
svg: "https://beletage.ro/img/Green-slash.svg",
},
address: [...ADDR_UNIRII],
website: 'www.beletage.ro',
motto: 'we make complex simple',
logoDimensions: { width: 162, height: 24 },
address: [...ADDR_CHRISTESCU],
website: "www.beletage.ro",
motto: "we make complex simple",
defaultColors: BELETAGE_COLORS,
},
'urban-switch': {
id: 'urban-switch',
name: 'Urban Switch SRL',
accent: '#6366f1',
"urban-switch": {
id: "urban-switch",
name: "Urban Switch SRL",
accent: "#345476",
logo: {
png: '/logos/logo-us-dark.svg',
svg: '/logos/logo-us-dark.svg',
png: "/logos/logo-us-light.svg",
svg: "/logos/logo-us-light.svg",
},
slashGrey: {
png: 'https://beletage.ro/img/Grey-slash.png',
svg: 'https://beletage.ro/img/Grey-slash.svg',
png: "https://beletage.ro/img/Grey-slash.png",
svg: "https://beletage.ro/img/Grey-slash.svg",
},
slashAccent: {
png: '/logos/logo-us-light.svg',
svg: '/logos/logo-us-light.svg',
},
address: ['str. Unirii, nr. 3, ap. 26', 'Cluj-Napoca, Cluj 400417', 'România'],
website: 'www.urbanswitch.ro',
motto: 'shaping urban futures',
slashAccent: { png: "", svg: "" },
logoDimensions: { width: 140, height: 24 },
address: [...ADDR_CHRISTESCU],
website: "www.urbanswitch.ro",
motto: "shaping urban futures",
defaultColors: URBAN_SWITCH_COLORS,
},
'studii-de-teren': {
id: 'studii-de-teren',
name: 'Studii de Teren SRL',
accent: '#f59e0b',
"studii-de-teren": {
id: "studii-de-teren",
name: "Studii de Teren SRL",
accent: "#0182A1",
logo: {
png: '/logos/logo-sdt-dark.svg',
svg: '/logos/logo-sdt-dark.svg',
png: "/logos/logo-sdt-light.svg",
svg: "/logos/logo-sdt-light.svg",
},
slashGrey: {
png: 'https://beletage.ro/img/Grey-slash.png',
svg: 'https://beletage.ro/img/Grey-slash.svg',
png: "https://beletage.ro/img/Grey-slash.png",
svg: "https://beletage.ro/img/Grey-slash.svg",
},
slashAccent: {
png: '/logos/logo-sdt-light.svg',
svg: '/logos/logo-sdt-light.svg',
},
address: ['str. Unirii, nr. 3, ap. 26', 'Cluj-Napoca, Cluj 400417', 'România'],
website: 'www.studiideteren.ro',
motto: 'ground truth, measured right',
slashAccent: { png: "", svg: "" },
logoDimensions: { width: 71, height: 24 },
address: [...ADDR_CHRISTESCU],
website: "www.studiideteren.ro",
motto: "ground truth, measured right",
defaultColors: STUDII_COLORS,
},
group: {
id: 'group',
name: 'Grup Companii',
accent: '#64748b',
logo: { png: '', svg: '' },
id: "group",
name: "Grup Companii",
accent: "#64748b",
logo: { png: "", svg: "" },
slashGrey: {
png: 'https://beletage.ro/img/Grey-slash.png',
svg: 'https://beletage.ro/img/Grey-slash.svg',
png: "https://beletage.ro/img/Grey-slash.png",
svg: "https://beletage.ro/img/Grey-slash.svg",
},
slashAccent: { png: '', svg: '' },
address: ['Cluj-Napoca, Cluj', 'România'],
website: '',
motto: '',
slashAccent: { png: "", svg: "" },
address: ["Cluj-Napoca, Cluj", "România"],
website: "",
motto: "",
defaultColors: BELETAGE_COLORS,
},
};

View File

@@ -1,9 +1,9 @@
import type { SignatureConfig, CompanyBranding } from '../types';
import { getBranding } from './company-branding';
import type { SignatureConfig, CompanyBranding } from "../types";
import { getBranding } from "./company-branding";
export function formatPhone(raw: string): { display: string; link: string } {
const clean = raw.replace(/\s/g, '');
if (clean.length === 10 && clean.startsWith('07')) {
const clean = raw.replace(/\s/g, "");
if (clean.length === 10 && clean.startsWith("07")) {
return {
display: `+40 ${clean.substring(1, 4)} ${clean.substring(4, 7)} ${clean.substring(7, 10)}`,
link: `tel:+40${clean.substring(1)}`,
@@ -17,30 +17,47 @@ export function generateSignatureHtml(config: SignatureConfig): string {
const address = config.addressOverride ?? branding.address;
const { display: phone, link: phoneLink } = formatPhone(config.phone);
const images = config.useSvg
? { logo: branding.logo.svg, greySlash: branding.slashGrey.svg, accentSlash: branding.slashAccent.svg }
: { logo: branding.logo.png, greySlash: branding.slashGrey.png, accentSlash: branding.slashAccent.png };
? {
logo: branding.logo.svg,
greySlash: branding.slashGrey.svg,
accentSlash: branding.slashAccent.svg,
}
: {
logo: branding.logo.png,
greySlash: branding.slashGrey.png,
accentSlash: branding.slashAccent.png,
};
const {
greenLineWidth, gutterWidth, iconTextSpacing, iconVerticalOffset,
mottoSpacing, sectionSpacing, titleSpacing, logoSpacing,
greenLineWidth,
gutterWidth,
iconTextSpacing,
iconVerticalOffset,
mottoSpacing,
sectionSpacing,
titleSpacing,
logoSpacing,
} = config.layout;
const colors = config.colors;
const isReply = config.variant === 'reply' || config.variant === 'minimal';
const isMinimal = config.variant === 'minimal';
const isReply = config.variant === "reply" || config.variant === "minimal";
const isMinimal = config.variant === "minimal";
const hide = 'mso-hide:all;display:none!important;max-height:0;overflow:hidden;font-size:0;line-height:0;';
const hideTitle = isReply ? hide : '';
const hideLogo = isReply ? hide : '';
const hideBottom = isMinimal ? hide : '';
const hidePhoneIcon = isMinimal ? hide : '';
const logoDim = branding.logoDimensions ?? { width: 162, height: 24 };
const hide =
"mso-hide:all;display:none!important;max-height:0;overflow:hidden;font-size:0;line-height:0;";
const hideTitle = isReply ? hide : "";
const hideLogo = isReply ? hide : "";
const hideBottom = isMinimal ? hide : "";
const hidePhoneIcon = isMinimal ? hide : "";
const spacerWidth = Math.max(0, iconTextSpacing);
const textPaddingLeft = Math.max(0, -iconTextSpacing);
const prefixHtml = config.prefix
? `<span style="font-size:13px; color:${colors.prefix};">${esc(config.prefix)} </span>`
: '';
: "";
return `<table role="presentation" cellpadding="0" cellspacing="0" border="0" width="540" style="font-family: Arial, Helvetica, sans-serif; color:#333333; font-size:14px; line-height:18px;">
<tbody>
@@ -57,28 +74,32 @@ export function generateSignatureHtml(config: SignatureConfig): string {
</td>
</tr>
<tr style="${hideLogo}"><td style="padding:${logoSpacing}px 0 ${logoSpacing + 2}px 0;">
${images.logo ? `<a href="https://${branding.website}" style="text-decoration:none; border:0;">
<img src="${images.logo}" alt="${esc(branding.name)}" style="display:block; border:0; height:24px; width:162px;" height="24" width="162">
</a>` : ''}
${
images.logo
? `<a href="https://${branding.website}" style="text-decoration:none; border:0;">
<img src="${images.logo}" alt="${esc(branding.name)}" style="display:block; border:0; height:${logoDim.height}px; width:${logoDim.width}px;" height="${logoDim.height}" width="${logoDim.width}">
</a>`
: ""
}
</td></tr>
<tr>
<td style="padding-top:${hideLogo ? '0' : sectionSpacing}px;">
<td style="padding-top:${hideLogo ? "0" : sectionSpacing}px;">
<table role="presentation" cellpadding="0" cellspacing="0" border="0" width="540" style="font-size:13px; line-height:18px;">
<tbody>
<tr style="${hideLogo}">
<td width="${gutterWidth}" style="width:${gutterWidth}px; font-size:0; line-height:0;"></td>
<td width="11" style="width:11px; vertical-align:top; padding-top:${4 + iconVerticalOffset}px;">
${images.greySlash ? `<img src="${images.greySlash}" alt="" width="11" height="11" style="display:block; border:0;">` : ''}
${images.greySlash ? `<img src="${images.greySlash}" alt="" width="11" height="11" style="display:block; border:0;">` : ""}
</td>
<td width="${spacerWidth}" style="width:${spacerWidth}px; font-size:0; line-height:0;"></td>
<td style="vertical-align:top; padding:0 0 0 ${textPaddingLeft}px;">
<span style="color:${colors.address}; text-decoration:none;">${address.join('<br>')}</span>
<span style="color:${colors.address}; text-decoration:none;">${address.join("<br>")}</span>
</td>
</tr>
<tr>
<td width="${gutterWidth}" style="width:${gutterWidth}px; font-size:0; line-height:0;"></td>
<td width="11" style="width:11px; vertical-align:top; padding-top:${12 + iconVerticalOffset}px; ${hidePhoneIcon}">
${images.accentSlash ? `<img src="${images.accentSlash}" alt="" width="11" height="7" style="display:block; border:0;">` : ''}
${images.accentSlash ? `<img src="${images.accentSlash}" alt="" width="11" height="7" style="display:block; border:0;">` : ""}
</td>
<td width="${isMinimal ? 0 : spacerWidth}" style="width:${isMinimal ? 0 : spacerWidth}px; font-size:0; line-height:0;"></td>
<td style="vertical-align:top; padding:8px 0 0 ${isMinimal ? 0 : textPaddingLeft}px;">
@@ -89,7 +110,7 @@ export function generateSignatureHtml(config: SignatureConfig): string {
</table>
</td>
</tr>
${branding.website ? `<tr style="${hideBottom}"><td style="padding:${sectionSpacing}px 0 ${mottoSpacing}px 0;"><a href="https://${branding.website}" style="color:${colors.website}; text-decoration:none;"><span style="color:${colors.website}; text-decoration:none;">${branding.website}</span></a></td></tr>` : ''}
${branding.website ? `<tr style="${hideBottom}"><td style="padding:${sectionSpacing}px 0 ${mottoSpacing}px 0;"><a href="https://${branding.website}" style="color:${colors.website}; text-decoration:none;"><span style="color:${colors.website}; text-decoration:none;">${branding.website}</span></a></td></tr>` : ""}
<tr style="${hideBottom}">
<td style="padding:0; font-size:0; line-height:0;">
<table role="presentation" cellpadding="0" cellspacing="0" border="0" width="540">
@@ -100,22 +121,22 @@ export function generateSignatureHtml(config: SignatureConfig): string {
</table>
</td>
</tr>
${branding.motto ? `<tr style="${hideBottom}"><td style="padding:${mottoSpacing}px 0 0 0;"><span style="font-size:12px; color:${colors.motto}; font-style:italic;">${esc(branding.motto)}</span></td></tr>` : ''}
${branding.motto ? `<tr style="${hideBottom}"><td style="padding:${mottoSpacing}px 0 0 0;"><span style="font-size:12px; color:${colors.motto}; font-style:italic;">${esc(branding.motto)}</span></td></tr>` : ""}
</tbody>
</table>`;
}
function esc(text: string): string {
return text
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;");
}
export function downloadSignatureHtml(html: string, filename: string): void {
const blob = new Blob([html], { type: 'text/html' });
const a = document.createElement('a');
const blob = new Blob([html], { type: "text/html" });
const a = document.createElement("a");
a.href = URL.createObjectURL(blob);
a.download = filename;
document.body.appendChild(a);

View File

@@ -1,6 +1,6 @@
import type { CompanyId } from '@/core/auth/types';
import type { CompanyId } from "@/core/auth/types";
export type SignatureVariant = 'full' | 'reply' | 'minimal';
export type SignatureVariant = "full" | "reply" | "minimal";
export interface SignatureColors {
prefix: string;
@@ -30,6 +30,8 @@ export interface CompanyBranding {
logo: { png: string; svg: string };
slashGrey: { png: string; svg: string };
slashAccent: { png: string; svg: string };
/** Logo dimensions (width × height) for the signature HTML */
logoDimensions?: { width: number; height: number };
address: string[];
website: string;
motto: string;