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:
35
ROADMAP.md
35
ROADMAP.md
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"');
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """);
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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" />
|
||||
|
||||
Reference in New Issue
Block a user