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

@@ -19,7 +19,7 @@
## AI Model Recommendations
| Tag | Claude | OpenAI | Google | Best For |
|---|---|---|---|---|
| ------------ | ---------- | ------------- | ---------------- | ---------------------------------------------------------------------- |
| `[HEAVY]` | Opus 4.6 | GPT-5.3-Codex | Gemini 3 Pro | Complex multi-file features, business logic, architecture, new modules |
| `[STANDARD]` | Sonnet 4.6 | GPT-5.2 | Gemini 3 Flash | Refactoring, moderate features, UI work, tests, documentation |
| `[LIGHT]` | Haiku 4.5 | GPT-4o-mini | Gemini 2.5 Flash | Quick fixes, small edits, config changes, build debugging |
@@ -31,7 +31,7 @@
## Current Module Status vs. XLSX Spec
| # | Module | Core Done | Gaps Remaining | New Features Needed |
|---|---|---|---|---|
| --- | ------------------ | ----------- | -------------------------------------------------------------------------------- | ------------------------------------------- |
| 1 | Registratura | YES | Linked-entry selector capped at 20 | Workflow automation, email integration, OCR |
| 2 | Email Signature | YES | US/SDT logo files may be missing from `/public/logos/`; US/SDT no address toggle | AD sync, branding packs |
| 3 | Word XML | YES | POT/CUT toggle exists (spec says remove) | Schema validator, visual mapper |
@@ -75,6 +75,7 @@
### 1.03 `[STANDARD]` Prompt Generator — Architecture Visualization Templates
**What:** Add 6+ new builtin templates per xlsx spec:
1. Architectural rendering prompt (basic massing to detailed)
2. Sketch → professional render prompt
3. Visualization refinement prompt (photorealism fine-tuning)
@@ -94,11 +95,13 @@
### 1.04 `[STANDARD]` Tag Manager — US/SDT Project Seeds + Mandatory Categories
**What:**
1. Add Urban Switch and Studii de Teren project numbering to seed data (US-001, SDT-001 format)
2. Enforce mandatory 1st category (project) and 2nd category (phase) when creating tags — show validation error if missing
3. Import the full tag structure from `legacy/manicprojects/current manic time Tags.txt` in proper 1st→5th category hierarchy
**Files to modify:**
- `src/modules/tag-manager/services/seed-data.ts` — Add US/SDT projects
- `src/modules/tag-manager/components/tag-create-form.tsx` — Add mandatory validation
@@ -107,6 +110,7 @@
### 1.05 `[STANDARD]` Mini Utilities — Add Missing Tools
**What:** Add the 5 missing tools from xlsx:
1. **U-value → R-value converter** (R = 1/U, with material thickness input)
2. **AI artifact cleaner** (strip markdown formatting, fix encoding, remove prompt artifacts from pasted text)
3. **MDLPA date locale validator** (validate Romanian administrative dates against legal calendar)
@@ -121,10 +125,12 @@
### 1.06 `[STANDARD]` Digital Signatures — File Upload + Tag Editing
**What:**
1. Add drag-and-drop / file picker for uploading signature/stamp images (convert to base64 on upload, like Registratura attachments)
2. Add tag input field to the asset form (tags field exists in type but form doesn't render it)
**Files to modify:**
- `src/modules/digital-signatures/components/` — asset form component
---
@@ -132,11 +138,13 @@
### 1.07 `[LIGHT]` Password Vault — Company Scope + Strength Meter
**What:**
1. Add `company` field to credential type and form (scope passwords to a company)
2. Add password strength indicator (visual bar: weak/medium/strong based on length + character diversity)
3. Rename `encryptedPassword``password` in the type (it's not encrypted, the name is misleading)
**Files to modify:**
- `src/modules/password-vault/types.ts`
- `src/modules/password-vault/components/` — form and list components
@@ -146,6 +154,7 @@
**What:** Change `assignedTo` from free text to an autocomplete that links to Address Book contacts (same pattern as Registratura sender/recipient).
**Files to modify:**
- `src/modules/it-inventory/components/` — equipment form
- `src/modules/it-inventory/types.ts` — Add `assignedToContactId?: string`
@@ -154,10 +163,12 @@
### 1.09 `[STANDARD]` Address Book — vCard Export + Registratura Reverse Lookup
**What:**
1. Add "Export vCard" button per contact (generate `.vcf` file download)
2. Add a section showing Registratura entries where this contact appears as sender or recipient
**Files to modify:**
- `src/modules/address-book/components/` — contact card/detail view
**Files to create:**
- `src/modules/address-book/services/vcard-export.ts`
@@ -168,6 +179,7 @@
**What:** When a template file URL points to a `.docx`, parse it client-side to extract `{{placeholder}}` patterns and auto-populate the `placeholders[]` field. Use JSZip (already installed) to read the docx XML.
**Files to modify:**
- `src/modules/word-templates/components/` — template form
**Files to create:**
- `src/modules/word-templates/services/placeholder-parser.ts`
@@ -177,6 +189,7 @@
### 1.11 `[STANDARD]` Dashboard — Activity Feed + KPI Panels
**What:**
1. Add an activity feed showing recent actions across modules (last 20 creates/updates/deletes from localStorage timestamps)
2. Add KPI cards: entries this week, deadlines this week, overdue count, contacts added this month
3. Wire the `DashboardWidget` type that already exists in `types.ts`
@@ -207,6 +220,7 @@
### 2.01 `[HEAVY]` Hot Desk Module — Full Implementation
**What:** Build Module 14 from scratch per xlsx spec:
- 4 desks in a shared room
- Users reserve desks 1 week ahead
- Calendar view showing desk availability per day
@@ -215,6 +229,7 @@
- Visual room layout showing which desks are booked
**Module structure:**
```
src/modules/hot-desk/
├── components/
@@ -232,6 +247,7 @@ src/modules/hot-desk/
```
**Files to also create/modify:**
- `src/app/(modules)/hot-desk/page.tsx` — Route
- `src/config/modules.ts` — Register module
- `src/config/navigation.ts` — Add sidebar entry
@@ -248,9 +264,11 @@ src/modules/hot-desk/
### 3.01 `[STANDARD]` Install Testing Framework (Vitest)
**What:** Install and configure Vitest with React Testing Library.
```bash
npm install -D vitest @testing-library/react @testing-library/jest-dom jsdom @vitest/coverage-v8
```
**Files to create:** `vitest.config.ts`, `src/test-setup.ts`
**Files to modify:** `package.json` (add test scripts)
@@ -259,6 +277,7 @@ npm install -D vitest @testing-library/react @testing-library/jest-dom jsdom @vi
### 3.02 `[STANDARD]` Unit Tests — Critical Services
**What:** Write tests for the most critical business logic:
1. `working-days.test.ts` — Orthodox Easter 2024-2030, addWorkingDays, backward deadlines
2. `deadline-service.test.ts` — Due date computation, tacit approval, chain resolution
3. `registry-service.test.ts` — Number generation, overdue calculation
@@ -272,6 +291,7 @@ npm install -D vitest @testing-library/react @testing-library/jest-dom jsdom @vi
### 3.03 `[STANDARD]` Data Export/Import for All Modules
**What:** Create a shared utility for backing up localStorage data:
1. Per-module JSON export (download file)
2. Per-module JSON import (upload + merge)
3. Full backup: export ALL modules as single JSON
@@ -284,6 +304,7 @@ npm install -D vitest @testing-library/react @testing-library/jest-dom jsdom @vi
### 3.04 `[LIGHT]` Update Stale Documentation
**What:** Update docs to reflect current state:
- `docs/architecture/SYSTEM-ARCHITECTURE.md` — Change modules from "Planned" to "Implemented"
- `docs/DATA-MODEL.md` — Add TrackedDeadline, Hot Desk schemas
- `docs/REPO-STRUCTURE.md` — Add new files
@@ -303,6 +324,7 @@ npm install -D vitest @testing-library/react @testing-library/jest-dom jsdom @vi
### 4.01 `[HEAVY]` AI Chat — Real API Integration
**What:** Replace demo mode with actual AI provider calls:
- Create `/api/ai/chat` server-side route (API keys never exposed to browser)
- Provider abstraction: Anthropic Claude, OpenAI GPT, Ollama (local)
- Response streaming via ReadableStream
@@ -310,6 +332,7 @@ npm install -D vitest @testing-library/react @testing-library/jest-dom jsdom @vi
- Token usage display
**Env vars:**
```
ANTHROPIC_API_KEY=sk-ant-...
OPENAI_API_KEY=sk-...
@@ -325,6 +348,7 @@ AI_DEFAULT_MODEL=claude-sonnet-4-6-20261001
### 4.02 `[STANDARD]` AI Chat — Domain-Specific System Prompts
**What:** Architecture office-focused conversation modes:
- Romanian construction law assistant
- Architectural visualization prompt crafter
- Technical specification writer
@@ -347,12 +371,14 @@ AI_DEFAULT_MODEL=claude-sonnet-4-6-20261001
### 5.01 `[HEAVY]` Authentik OIDC Integration
**What:** Replace stub user with real Authentik SSO.
- NextAuth.js / Auth.js route handler
- OIDC token → user profile resolution
- Cookie-based session
- `useAuth()` returns real user
**Server setup required:**
1. Create OAuth2 app in Authentik (http://10.10.10.166:9100)
2. Set redirect URI: `http://10.10.10.166:3000/api/auth/callback/authentik`
3. Set env vars: `AUTHENTIK_URL`, `AUTHENTIK_CLIENT_ID`, `AUTHENTIK_CLIENT_SECRET`, `NEXTAUTH_SECRET`
@@ -500,7 +526,7 @@ AI_DEFAULT_MODEL=claude-sonnet-4-6-20261001
## Infrastructure Credentials Needed
| Service | What | When Needed |
|---|---|---|
| ------------------------ | --------------------------------------- | ------------------- |
| **US/SDT Logos** | SVG/PNG logo files | Phase 1 (task 1.01) |
| **US/SDT Addresses** | Office addresses for email signature | Phase 1 (task 1.02) |
| **Anthropic API Key** | `sk-ant-...` from console.anthropic.com | Phase 4 (task 4.01) |
@@ -515,6 +541,7 @@ AI_DEFAULT_MODEL=claude-sonnet-4-6-20261001
## Quick Picker
**15 min tasks** `[LIGHT]`:
- 1.01 — Check logo files
- 1.07 — Password vault company + strength
- 1.08 — IT inventory contact link
@@ -524,6 +551,7 @@ AI_DEFAULT_MODEL=claude-sonnet-4-6-20261001
- 3.05 — Wire env var URLs
**1 hour tasks** `[STANDARD]`:
- 1.03 — Prompt generator templates
- 1.04 — Tag manager seeds + mandatory
- 1.05 — Mini utilities new tools
@@ -533,6 +561,7 @@ AI_DEFAULT_MODEL=claude-sonnet-4-6-20261001
- 3.01 + 3.02 — Tests setup + core tests
**Full session tasks** `[HEAVY]`:
- 2.01 — Hot Desk module (new)
- 4.01 — AI Chat API integration
- 5.01 — Authentik SSO

View File

@@ -7,6 +7,7 @@
## Session — 2026-02-18 (GitHub Copilot - Haiku 4.5)
### Completed
- **Task 1.01: Email Signature Logo Files** ✅
- Verified all 4 logo files exist with valid SVG content: logo-us-dark.svg, logo-us-light.svg, logo-sdt-dark.svg, logo-sdt-light.svg
- No action needed — logos are already present and valid
@@ -18,9 +19,11 @@
- Build passes zero errors
### Commits
- `1db61d8` feat(email-signature): add address toggles for Urban Switch and Studii de Teren
### Notes
- Full npm install and build verification completed
- Ready to move to task 1.03 (Prompt Generator architecture templates) after user approval
- Set up git with AI Assistant user for commits
@@ -30,6 +33,7 @@
## Session — 2026-02-18 (Claude Opus 4.6)
### Completed
- **Registratura Legal Deadline Tracking** — Full implementation:
- 9 new files: working-days.ts (Romanian holidays + Orthodox Easter), deadline-catalog.ts (16 deadline types), deadline-service.ts, use-deadline-filters.ts, deadline-card.tsx, deadline-add-dialog.tsx, deadline-resolve-dialog.tsx, deadline-table.tsx, deadline-dashboard.tsx
- 6 modified files: types.ts, use-registry.ts, registratura-module.tsx (tabbed), registry-entry-form.tsx (inline deadlines), registry-table.tsx (clock badge), index.ts
@@ -39,21 +43,22 @@
- **SESSION-GUIDE.md** — Created with start/resume prompts, git workflow, file update rules
### Commits
- `bb01268` feat(registratura): add legal deadline tracking system (Termene Legale)
- `d6a5852` docs: add ROADMAP.md with detailed future task plan
- `b1df15b` docs: rewrite ROADMAP.md with complete xlsx gap analysis + multi-model recommendations
- (this session) docs: add SESSION-GUIDE.md + SESSION-LOG.md
### Notes
- Build passes with zero errors
- Dev server on localhost:3000 shows tabs correctly
- Production at 10.10.10.166:3000 requires Portainer redeploy after push
- The `app_modules_overview.xlsx` is in the repo root but not committed (it's a reference file)
- No tasks from ROADMAP.md Phase 1+ have been started yet — next session should begin with task 1.01
### Completed
- **Registratura Legal Deadline Tracking** — Full implementation:
- 9 new files: working-days.ts (Romanian holidays + Orthodox Easter), deadline-catalog.ts (16 deadline types), deadline-service.ts, use-deadline-filters.ts, deadline-card.tsx, deadline-add-dialog.tsx, deadline-resolve-dialog.tsx, deadline-table.tsx, deadline-dashboard.tsx
- 6 modified files: types.ts, use-registry.ts, registratura-module.tsx (tabbed), registry-entry-form.tsx (inline deadlines), registry-table.tsx (clock badge), index.ts
@@ -63,12 +68,14 @@
- **SESSION-GUIDE.md** — Created with start/resume prompts, git workflow, file update rules
### Commits
- `bb01268` feat(registratura): add legal deadline tracking system (Termene Legale)
- `d6a5852` docs: add ROADMAP.md with detailed future task plan
- `b1df15b` docs: rewrite ROADMAP.md with complete xlsx gap analysis + multi-model recommendations
- (this session) docs: add SESSION-GUIDE.md + SESSION-LOG.md
### Notes
- Build passes with zero errors
- Dev server on localhost:3000 shows tabs correctly
- Production at 10.10.10.166:3000 requires Portainer redeploy after push

View File

@@ -1,4 +1,4 @@
import type { CompanyId } from '@/core/auth/types';
import type { CompanyId } from "@/core/auth/types";
export interface Company {
id: CompanyId;
@@ -16,48 +16,48 @@ export interface Company {
export const COMPANIES: Record<CompanyId, Company> = {
beletage: {
id: 'beletage',
name: 'Beletage SRL',
shortName: 'Beletage',
cui: '',
color: '#22B5AB',
address: 'str. Unirii, nr. 3, ap. 26',
city: 'Cluj-Napoca',
id: "beletage",
name: "Beletage SRL",
shortName: "Beletage",
cui: "",
color: "#22B5AB",
address: "str. Unirii, nr. 3, ap. 26",
city: "Cluj-Napoca",
},
'urban-switch': {
id: 'urban-switch',
name: 'Urban Switch SRL',
shortName: 'Urban Switch',
cui: '',
color: '#6366f1',
address: '',
city: 'Cluj-Napoca',
"urban-switch": {
id: "urban-switch",
name: "Urban Switch SRL",
shortName: "Urban Switch",
cui: "",
color: "#6366f1",
address: "",
city: "Cluj-Napoca",
logo: {
light: '/logos/logo-us-light.svg',
dark: '/logos/logo-us-dark.svg',
light: "/logos/logo-us-light.svg",
dark: "/logos/logo-us-light.svg",
},
},
'studii-de-teren': {
id: 'studii-de-teren',
name: 'Studii de Teren SRL',
shortName: 'Studii de Teren',
cui: '',
color: '#f59e0b',
address: '',
city: 'Cluj-Napoca',
"studii-de-teren": {
id: "studii-de-teren",
name: "Studii de Teren SRL",
shortName: "Studii de Teren",
cui: "",
color: "#f59e0b",
address: "",
city: "Cluj-Napoca",
logo: {
light: '/logos/logo-sdt-dark.svg',
dark: '/logos/logo-sdt-light.svg',
light: "/logos/logo-sdt-light.svg",
dark: "/logos/logo-sdt-light.svg",
},
},
group: {
id: 'group',
name: 'Grup Companii',
shortName: 'Grup',
cui: '',
color: '#64748b',
address: '',
city: 'Cluj-Napoca',
id: "group",
name: "Grup Companii",
shortName: "Grup",
cui: "",
color: "#64748b",
address: "",
city: "Cluj-Napoca",
},
};

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,9 +327,12 @@ 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) => (
{(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>
<span className="text-sm text-muted-foreground">
{COLOR_LABELS[colorKey]}
</span>
<div className="flex gap-1.5">
{Object.values(palette).map((color) => (
<button
@@ -212,17 +340,18 @@ export function SignatureConfigurator({
type="button"
onClick={() => onUpdateColor(colorKey, color)}
className={cn(
'h-6 w-6 rounded-full border-2 transition-all',
"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'
? "border-primary scale-110 ring-2 ring-primary/30"
: "border-transparent hover:scale-105",
)}
style={{ backgroundColor: color }}
/>
))}
</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;

View File

@@ -1,26 +1,50 @@
'use client';
"use client";
import Image from 'next/image';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
import { useTheme } from 'next-themes';
import { useMemo } from 'react';
import * as Icons from 'lucide-react';
import { buildNavigation } from '@/config/navigation';
import { COMPANIES } from '@/config/companies';
import { useFeatureFlag } from '@/core/feature-flags';
import { cn } from '@/shared/lib/utils';
import { ScrollArea } from '@/shared/components/ui/scroll-area';
import { Separator } from '@/shared/components/ui/separator';
import Image from "next/image";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { useMemo } from "react";
import * as Icons from "lucide-react";
import { buildNavigation } from "@/config/navigation";
import { COMPANIES } from "@/config/companies";
import { useFeatureFlag } from "@/core/feature-flags";
import { cn } from "@/shared/lib/utils";
import { ScrollArea } from "@/shared/components/ui/scroll-area";
import { Separator } from "@/shared/components/ui/separator";
function DynamicIcon({ name, className }: { name: string; className?: string }) {
const pascalName = name.replace(/(^|-)([a-z])/g, (_, _p, c: string) => c.toUpperCase());
const IconComponent = (Icons as unknown as Record<string, React.ComponentType<{ className?: string }>>)[pascalName];
function DynamicIcon({
name,
className,
}: {
name: string;
className?: string;
}) {
const pascalName = name.replace(/(^|-)([a-z])/g, (_, _p, c: string) =>
c.toUpperCase(),
);
const IconComponent = (
Icons as unknown as Record<
string,
React.ComponentType<{ className?: string }>
>
)[pascalName];
if (!IconComponent) return <Icons.Circle className={className} />;
return <IconComponent className={className} />;
}
function NavItem({ item, isActive }: { item: { id: string; label: string; icon: string; href: string; featureFlag: string }; isActive: boolean }) {
function NavItem({
item,
isActive,
}: {
item: {
id: string;
label: string;
icon: string;
href: string;
featureFlag: string;
};
isActive: boolean;
}) {
const enabled = useFeatureFlag(item.featureFlag);
if (!enabled) return null;
@@ -28,10 +52,10 @@ function NavItem({ item, isActive }: { item: { id: string; label: string; icon:
<Link
href={item.href}
className={cn(
'flex items-center gap-3 rounded-md px-3 py-2 text-sm transition-colors',
"flex items-center gap-3 rounded-md px-3 py-2 text-sm transition-colors",
isActive
? 'bg-accent text-accent-foreground font-medium'
: 'text-muted-foreground hover:bg-accent/50 hover:text-accent-foreground'
? "bg-accent text-accent-foreground font-medium"
: "text-muted-foreground hover:bg-accent/50 hover:text-accent-foreground",
)}
>
<DynamicIcon name={item.icon} className="h-4 w-4 shrink-0" />
@@ -41,11 +65,9 @@ function NavItem({ item, isActive }: { item: { id: string; label: string; icon:
}
function SidebarLogo() {
const { resolvedTheme } = useTheme();
const sdt = COMPANIES['studii-de-teren'];
const logoSrc = sdt.logo
? (resolvedTheme === 'dark' ? sdt.logo.dark : sdt.logo.light)
: null;
const sdt = COMPANIES["studii-de-teren"];
const logoSrc = sdt.logo?.light ?? null;
if (!logoSrc) {
return <Icons.LayoutDashboard className="h-5 w-5 text-primary" />;
@@ -58,6 +80,7 @@ function SidebarLogo() {
width={28}
height={28}
className="h-7 w-7 shrink-0"
suppressHydrationWarning
/>
);
}
@@ -77,10 +100,10 @@ export function Sidebar() {
<Link
href="/"
className={cn(
'mb-1 flex items-center gap-3 rounded-md px-3 py-2 text-sm transition-colors',
pathname === '/'
? 'bg-accent text-accent-foreground font-medium'
: 'text-muted-foreground hover:bg-accent/50 hover:text-accent-foreground'
"mb-1 flex items-center gap-3 rounded-md px-3 py-2 text-sm transition-colors",
pathname === "/"
? "bg-accent text-accent-foreground font-medium"
: "text-muted-foreground hover:bg-accent/50 hover:text-accent-foreground",
)}
>
<Icons.Home className="h-4 w-4" />