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:
39
ROADMAP.md
39
ROADMAP.md
@@ -19,7 +19,7 @@
|
|||||||
## AI Model Recommendations
|
## AI Model Recommendations
|
||||||
|
|
||||||
| Tag | Claude | OpenAI | Google | Best For |
|
| 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 |
|
| `[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 |
|
| `[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 |
|
| `[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
|
## Current Module Status vs. XLSX Spec
|
||||||
|
|
||||||
| # | Module | Core Done | Gaps Remaining | New Features Needed |
|
| # | Module | Core Done | Gaps Remaining | New Features Needed |
|
||||||
|---|---|---|---|---|
|
| --- | ------------------ | ----------- | -------------------------------------------------------------------------------- | ------------------------------------------- |
|
||||||
| 1 | Registratura | YES | Linked-entry selector capped at 20 | Workflow automation, email integration, OCR |
|
| 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 |
|
| 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 |
|
| 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
|
### 1.03 `[STANDARD]` Prompt Generator — Architecture Visualization Templates
|
||||||
|
|
||||||
**What:** Add 6+ new builtin templates per xlsx spec:
|
**What:** Add 6+ new builtin templates per xlsx spec:
|
||||||
|
|
||||||
1. Architectural rendering prompt (basic massing to detailed)
|
1. Architectural rendering prompt (basic massing to detailed)
|
||||||
2. Sketch → professional render prompt
|
2. Sketch → professional render prompt
|
||||||
3. Visualization refinement prompt (photorealism fine-tuning)
|
3. Visualization refinement prompt (photorealism fine-tuning)
|
||||||
@@ -94,11 +95,13 @@
|
|||||||
### 1.04 `[STANDARD]` Tag Manager — US/SDT Project Seeds + Mandatory Categories
|
### 1.04 `[STANDARD]` Tag Manager — US/SDT Project Seeds + Mandatory Categories
|
||||||
|
|
||||||
**What:**
|
**What:**
|
||||||
|
|
||||||
1. Add Urban Switch and Studii de Teren project numbering to seed data (US-001, SDT-001 format)
|
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
|
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
|
3. Import the full tag structure from `legacy/manicprojects/current manic time Tags.txt` in proper 1st→5th category hierarchy
|
||||||
|
|
||||||
**Files to modify:**
|
**Files to modify:**
|
||||||
|
|
||||||
- `src/modules/tag-manager/services/seed-data.ts` — Add US/SDT projects
|
- `src/modules/tag-manager/services/seed-data.ts` — Add US/SDT projects
|
||||||
- `src/modules/tag-manager/components/tag-create-form.tsx` — Add mandatory validation
|
- `src/modules/tag-manager/components/tag-create-form.tsx` — Add mandatory validation
|
||||||
|
|
||||||
@@ -107,6 +110,7 @@
|
|||||||
### 1.05 `[STANDARD]` Mini Utilities — Add Missing Tools
|
### 1.05 `[STANDARD]` Mini Utilities — Add Missing Tools
|
||||||
|
|
||||||
**What:** Add the 5 missing tools from xlsx:
|
**What:** Add the 5 missing tools from xlsx:
|
||||||
|
|
||||||
1. **U-value → R-value converter** (R = 1/U, with material thickness input)
|
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)
|
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)
|
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
|
### 1.06 `[STANDARD]` Digital Signatures — File Upload + Tag Editing
|
||||||
|
|
||||||
**What:**
|
**What:**
|
||||||
|
|
||||||
1. Add drag-and-drop / file picker for uploading signature/stamp images (convert to base64 on upload, like Registratura attachments)
|
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)
|
2. Add tag input field to the asset form (tags field exists in type but form doesn't render it)
|
||||||
|
|
||||||
**Files to modify:**
|
**Files to modify:**
|
||||||
|
|
||||||
- `src/modules/digital-signatures/components/` — asset form component
|
- `src/modules/digital-signatures/components/` — asset form component
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -132,11 +138,13 @@
|
|||||||
### 1.07 `[LIGHT]` Password Vault — Company Scope + Strength Meter
|
### 1.07 `[LIGHT]` Password Vault — Company Scope + Strength Meter
|
||||||
|
|
||||||
**What:**
|
**What:**
|
||||||
|
|
||||||
1. Add `company` field to credential type and form (scope passwords to a company)
|
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)
|
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)
|
3. Rename `encryptedPassword` → `password` in the type (it's not encrypted, the name is misleading)
|
||||||
|
|
||||||
**Files to modify:**
|
**Files to modify:**
|
||||||
|
|
||||||
- `src/modules/password-vault/types.ts`
|
- `src/modules/password-vault/types.ts`
|
||||||
- `src/modules/password-vault/components/` — form and list components
|
- `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).
|
**What:** Change `assignedTo` from free text to an autocomplete that links to Address Book contacts (same pattern as Registratura sender/recipient).
|
||||||
**Files to modify:**
|
**Files to modify:**
|
||||||
|
|
||||||
- `src/modules/it-inventory/components/` — equipment form
|
- `src/modules/it-inventory/components/` — equipment form
|
||||||
- `src/modules/it-inventory/types.ts` — Add `assignedToContactId?: string`
|
- `src/modules/it-inventory/types.ts` — Add `assignedToContactId?: string`
|
||||||
|
|
||||||
@@ -154,12 +163,14 @@
|
|||||||
### 1.09 `[STANDARD]` Address Book — vCard Export + Registratura Reverse Lookup
|
### 1.09 `[STANDARD]` Address Book — vCard Export + Registratura Reverse Lookup
|
||||||
|
|
||||||
**What:**
|
**What:**
|
||||||
|
|
||||||
1. Add "Export vCard" button per contact (generate `.vcf` file download)
|
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
|
2. Add a section showing Registratura entries where this contact appears as sender or recipient
|
||||||
|
|
||||||
**Files to modify:**
|
**Files to modify:**
|
||||||
|
|
||||||
- `src/modules/address-book/components/` — contact card/detail view
|
- `src/modules/address-book/components/` — contact card/detail view
|
||||||
**Files to create:**
|
**Files to create:**
|
||||||
- `src/modules/address-book/services/vcard-export.ts`
|
- `src/modules/address-book/services/vcard-export.ts`
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -168,8 +179,9 @@
|
|||||||
|
|
||||||
**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.
|
**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:**
|
**Files to modify:**
|
||||||
|
|
||||||
- `src/modules/word-templates/components/` — template form
|
- `src/modules/word-templates/components/` — template form
|
||||||
**Files to create:**
|
**Files to create:**
|
||||||
- `src/modules/word-templates/services/placeholder-parser.ts`
|
- `src/modules/word-templates/services/placeholder-parser.ts`
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -177,6 +189,7 @@
|
|||||||
### 1.11 `[STANDARD]` Dashboard — Activity Feed + KPI Panels
|
### 1.11 `[STANDARD]` Dashboard — Activity Feed + KPI Panels
|
||||||
|
|
||||||
**What:**
|
**What:**
|
||||||
|
|
||||||
1. Add an activity feed showing recent actions across modules (last 20 creates/updates/deletes from localStorage timestamps)
|
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
|
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`
|
3. Wire the `DashboardWidget` type that already exists in `types.ts`
|
||||||
@@ -207,6 +220,7 @@
|
|||||||
### 2.01 `[HEAVY]` Hot Desk Module — Full Implementation
|
### 2.01 `[HEAVY]` Hot Desk Module — Full Implementation
|
||||||
|
|
||||||
**What:** Build Module 14 from scratch per xlsx spec:
|
**What:** Build Module 14 from scratch per xlsx spec:
|
||||||
|
|
||||||
- 4 desks in a shared room
|
- 4 desks in a shared room
|
||||||
- Users reserve desks 1 week ahead
|
- Users reserve desks 1 week ahead
|
||||||
- Calendar view showing desk availability per day
|
- Calendar view showing desk availability per day
|
||||||
@@ -215,6 +229,7 @@
|
|||||||
- Visual room layout showing which desks are booked
|
- Visual room layout showing which desks are booked
|
||||||
|
|
||||||
**Module structure:**
|
**Module structure:**
|
||||||
|
|
||||||
```
|
```
|
||||||
src/modules/hot-desk/
|
src/modules/hot-desk/
|
||||||
├── components/
|
├── components/
|
||||||
@@ -232,6 +247,7 @@ src/modules/hot-desk/
|
|||||||
```
|
```
|
||||||
|
|
||||||
**Files to also create/modify:**
|
**Files to also create/modify:**
|
||||||
|
|
||||||
- `src/app/(modules)/hot-desk/page.tsx` — Route
|
- `src/app/(modules)/hot-desk/page.tsx` — Route
|
||||||
- `src/config/modules.ts` — Register module
|
- `src/config/modules.ts` — Register module
|
||||||
- `src/config/navigation.ts` — Add sidebar entry
|
- `src/config/navigation.ts` — Add sidebar entry
|
||||||
@@ -248,9 +264,11 @@ src/modules/hot-desk/
|
|||||||
### 3.01 `[STANDARD]` Install Testing Framework (Vitest)
|
### 3.01 `[STANDARD]` Install Testing Framework (Vitest)
|
||||||
|
|
||||||
**What:** Install and configure Vitest with React Testing Library.
|
**What:** Install and configure Vitest with React Testing Library.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm install -D vitest @testing-library/react @testing-library/jest-dom jsdom @vitest/coverage-v8
|
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 create:** `vitest.config.ts`, `src/test-setup.ts`
|
||||||
**Files to modify:** `package.json` (add test scripts)
|
**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
|
### 3.02 `[STANDARD]` Unit Tests — Critical Services
|
||||||
|
|
||||||
**What:** Write tests for the most critical business logic:
|
**What:** Write tests for the most critical business logic:
|
||||||
|
|
||||||
1. `working-days.test.ts` — Orthodox Easter 2024-2030, addWorkingDays, backward deadlines
|
1. `working-days.test.ts` — Orthodox Easter 2024-2030, addWorkingDays, backward deadlines
|
||||||
2. `deadline-service.test.ts` — Due date computation, tacit approval, chain resolution
|
2. `deadline-service.test.ts` — Due date computation, tacit approval, chain resolution
|
||||||
3. `registry-service.test.ts` — Number generation, overdue calculation
|
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
|
### 3.03 `[STANDARD]` Data Export/Import for All Modules
|
||||||
|
|
||||||
**What:** Create a shared utility for backing up localStorage data:
|
**What:** Create a shared utility for backing up localStorage data:
|
||||||
|
|
||||||
1. Per-module JSON export (download file)
|
1. Per-module JSON export (download file)
|
||||||
2. Per-module JSON import (upload + merge)
|
2. Per-module JSON import (upload + merge)
|
||||||
3. Full backup: export ALL modules as single JSON
|
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
|
### 3.04 `[LIGHT]` Update Stale Documentation
|
||||||
|
|
||||||
**What:** Update docs to reflect current state:
|
**What:** Update docs to reflect current state:
|
||||||
|
|
||||||
- `docs/architecture/SYSTEM-ARCHITECTURE.md` — Change modules from "Planned" to "Implemented"
|
- `docs/architecture/SYSTEM-ARCHITECTURE.md` — Change modules from "Planned" to "Implemented"
|
||||||
- `docs/DATA-MODEL.md` — Add TrackedDeadline, Hot Desk schemas
|
- `docs/DATA-MODEL.md` — Add TrackedDeadline, Hot Desk schemas
|
||||||
- `docs/REPO-STRUCTURE.md` — Add new files
|
- `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
|
### 4.01 `[HEAVY]` AI Chat — Real API Integration
|
||||||
|
|
||||||
**What:** Replace demo mode with actual AI provider calls:
|
**What:** Replace demo mode with actual AI provider calls:
|
||||||
|
|
||||||
- Create `/api/ai/chat` server-side route (API keys never exposed to browser)
|
- Create `/api/ai/chat` server-side route (API keys never exposed to browser)
|
||||||
- Provider abstraction: Anthropic Claude, OpenAI GPT, Ollama (local)
|
- Provider abstraction: Anthropic Claude, OpenAI GPT, Ollama (local)
|
||||||
- Response streaming via ReadableStream
|
- Response streaming via ReadableStream
|
||||||
@@ -310,6 +332,7 @@ npm install -D vitest @testing-library/react @testing-library/jest-dom jsdom @vi
|
|||||||
- Token usage display
|
- Token usage display
|
||||||
|
|
||||||
**Env vars:**
|
**Env vars:**
|
||||||
|
|
||||||
```
|
```
|
||||||
ANTHROPIC_API_KEY=sk-ant-...
|
ANTHROPIC_API_KEY=sk-ant-...
|
||||||
OPENAI_API_KEY=sk-...
|
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
|
### 4.02 `[STANDARD]` AI Chat — Domain-Specific System Prompts
|
||||||
|
|
||||||
**What:** Architecture office-focused conversation modes:
|
**What:** Architecture office-focused conversation modes:
|
||||||
|
|
||||||
- Romanian construction law assistant
|
- Romanian construction law assistant
|
||||||
- Architectural visualization prompt crafter
|
- Architectural visualization prompt crafter
|
||||||
- Technical specification writer
|
- Technical specification writer
|
||||||
@@ -347,12 +371,14 @@ AI_DEFAULT_MODEL=claude-sonnet-4-6-20261001
|
|||||||
### 5.01 `[HEAVY]` Authentik OIDC Integration
|
### 5.01 `[HEAVY]` Authentik OIDC Integration
|
||||||
|
|
||||||
**What:** Replace stub user with real Authentik SSO.
|
**What:** Replace stub user with real Authentik SSO.
|
||||||
|
|
||||||
- NextAuth.js / Auth.js route handler
|
- NextAuth.js / Auth.js route handler
|
||||||
- OIDC token → user profile resolution
|
- OIDC token → user profile resolution
|
||||||
- Cookie-based session
|
- Cookie-based session
|
||||||
- `useAuth()` returns real user
|
- `useAuth()` returns real user
|
||||||
|
|
||||||
**Server setup required:**
|
**Server setup required:**
|
||||||
|
|
||||||
1. Create OAuth2 app in Authentik (http://10.10.10.166:9100)
|
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`
|
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`
|
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
|
## Infrastructure Credentials Needed
|
||||||
|
|
||||||
| Service | What | When Needed |
|
| Service | What | When Needed |
|
||||||
|---|---|---|
|
| ------------------------ | --------------------------------------- | ------------------- |
|
||||||
| **US/SDT Logos** | SVG/PNG logo files | Phase 1 (task 1.01) |
|
| **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) |
|
| **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) |
|
| **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
|
## Quick Picker
|
||||||
|
|
||||||
**15 min tasks** `[LIGHT]`:
|
**15 min tasks** `[LIGHT]`:
|
||||||
|
|
||||||
- 1.01 — Check logo files
|
- 1.01 — Check logo files
|
||||||
- 1.07 — Password vault company + strength
|
- 1.07 — Password vault company + strength
|
||||||
- 1.08 — IT inventory contact link
|
- 1.08 — IT inventory contact link
|
||||||
@@ -524,6 +551,7 @@ AI_DEFAULT_MODEL=claude-sonnet-4-6-20261001
|
|||||||
- 3.05 — Wire env var URLs
|
- 3.05 — Wire env var URLs
|
||||||
|
|
||||||
**1 hour tasks** `[STANDARD]`:
|
**1 hour tasks** `[STANDARD]`:
|
||||||
|
|
||||||
- 1.03 — Prompt generator templates
|
- 1.03 — Prompt generator templates
|
||||||
- 1.04 — Tag manager seeds + mandatory
|
- 1.04 — Tag manager seeds + mandatory
|
||||||
- 1.05 — Mini utilities new tools
|
- 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
|
- 3.01 + 3.02 — Tests setup + core tests
|
||||||
|
|
||||||
**Full session tasks** `[HEAVY]`:
|
**Full session tasks** `[HEAVY]`:
|
||||||
|
|
||||||
- 2.01 — Hot Desk module (new)
|
- 2.01 — Hot Desk module (new)
|
||||||
- 4.01 — AI Chat API integration
|
- 4.01 — AI Chat API integration
|
||||||
- 5.01 — Authentik SSO
|
- 5.01 — Authentik SSO
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
## Session — 2026-02-18 (GitHub Copilot - Haiku 4.5)
|
## Session — 2026-02-18 (GitHub Copilot - Haiku 4.5)
|
||||||
|
|
||||||
### Completed
|
### Completed
|
||||||
|
|
||||||
- **Task 1.01: Email Signature Logo Files** ✅
|
- **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
|
- 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
|
- No action needed — logos are already present and valid
|
||||||
@@ -18,9 +19,11 @@
|
|||||||
- Build passes zero errors
|
- Build passes zero errors
|
||||||
|
|
||||||
### Commits
|
### Commits
|
||||||
|
|
||||||
- `1db61d8` feat(email-signature): add address toggles for Urban Switch and Studii de Teren
|
- `1db61d8` feat(email-signature): add address toggles for Urban Switch and Studii de Teren
|
||||||
|
|
||||||
### Notes
|
### Notes
|
||||||
|
|
||||||
- Full npm install and build verification completed
|
- Full npm install and build verification completed
|
||||||
- Ready to move to task 1.03 (Prompt Generator architecture templates) after user approval
|
- Ready to move to task 1.03 (Prompt Generator architecture templates) after user approval
|
||||||
- Set up git with AI Assistant user for commits
|
- Set up git with AI Assistant user for commits
|
||||||
@@ -30,6 +33,7 @@
|
|||||||
## Session — 2026-02-18 (Claude Opus 4.6)
|
## Session — 2026-02-18 (Claude Opus 4.6)
|
||||||
|
|
||||||
### Completed
|
### Completed
|
||||||
|
|
||||||
- **Registratura Legal Deadline Tracking** — Full implementation:
|
- **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
|
- 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
|
- 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
|
- **SESSION-GUIDE.md** — Created with start/resume prompts, git workflow, file update rules
|
||||||
|
|
||||||
### Commits
|
### Commits
|
||||||
|
|
||||||
- `bb01268` feat(registratura): add legal deadline tracking system (Termene Legale)
|
- `bb01268` feat(registratura): add legal deadline tracking system (Termene Legale)
|
||||||
- `d6a5852` docs: add ROADMAP.md with detailed future task plan
|
- `d6a5852` docs: add ROADMAP.md with detailed future task plan
|
||||||
- `b1df15b` docs: rewrite ROADMAP.md with complete xlsx gap analysis + multi-model recommendations
|
- `b1df15b` docs: rewrite ROADMAP.md with complete xlsx gap analysis + multi-model recommendations
|
||||||
- (this session) docs: add SESSION-GUIDE.md + SESSION-LOG.md
|
- (this session) docs: add SESSION-GUIDE.md + SESSION-LOG.md
|
||||||
|
|
||||||
### Notes
|
### Notes
|
||||||
|
|
||||||
- Build passes with zero errors
|
- Build passes with zero errors
|
||||||
- Dev server on localhost:3000 shows tabs correctly
|
- Dev server on localhost:3000 shows tabs correctly
|
||||||
- Production at 10.10.10.166:3000 requires Portainer redeploy after push
|
- 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)
|
- 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
|
- No tasks from ROADMAP.md Phase 1+ have been started yet — next session should begin with task 1.01
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### Completed
|
### Completed
|
||||||
|
|
||||||
- **Registratura Legal Deadline Tracking** — Full implementation:
|
- **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
|
- 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
|
- 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
|
- **SESSION-GUIDE.md** — Created with start/resume prompts, git workflow, file update rules
|
||||||
|
|
||||||
### Commits
|
### Commits
|
||||||
|
|
||||||
- `bb01268` feat(registratura): add legal deadline tracking system (Termene Legale)
|
- `bb01268` feat(registratura): add legal deadline tracking system (Termene Legale)
|
||||||
- `d6a5852` docs: add ROADMAP.md with detailed future task plan
|
- `d6a5852` docs: add ROADMAP.md with detailed future task plan
|
||||||
- `b1df15b` docs: rewrite ROADMAP.md with complete xlsx gap analysis + multi-model recommendations
|
- `b1df15b` docs: rewrite ROADMAP.md with complete xlsx gap analysis + multi-model recommendations
|
||||||
- (this session) docs: add SESSION-GUIDE.md + SESSION-LOG.md
|
- (this session) docs: add SESSION-GUIDE.md + SESSION-LOG.md
|
||||||
|
|
||||||
### Notes
|
### Notes
|
||||||
|
|
||||||
- Build passes with zero errors
|
- Build passes with zero errors
|
||||||
- Dev server on localhost:3000 shows tabs correctly
|
- Dev server on localhost:3000 shows tabs correctly
|
||||||
- Production at 10.10.10.166:3000 requires Portainer redeploy after push
|
- 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 {
|
export interface Company {
|
||||||
id: CompanyId;
|
id: CompanyId;
|
||||||
@@ -16,48 +16,48 @@ export interface Company {
|
|||||||
|
|
||||||
export const COMPANIES: Record<CompanyId, Company> = {
|
export const COMPANIES: Record<CompanyId, Company> = {
|
||||||
beletage: {
|
beletage: {
|
||||||
id: 'beletage',
|
id: "beletage",
|
||||||
name: 'Beletage SRL',
|
name: "Beletage SRL",
|
||||||
shortName: 'Beletage',
|
shortName: "Beletage",
|
||||||
cui: '',
|
cui: "",
|
||||||
color: '#22B5AB',
|
color: "#22B5AB",
|
||||||
address: 'str. Unirii, nr. 3, ap. 26',
|
address: "str. Unirii, nr. 3, ap. 26",
|
||||||
city: 'Cluj-Napoca',
|
city: "Cluj-Napoca",
|
||||||
},
|
},
|
||||||
'urban-switch': {
|
"urban-switch": {
|
||||||
id: 'urban-switch',
|
id: "urban-switch",
|
||||||
name: 'Urban Switch SRL',
|
name: "Urban Switch SRL",
|
||||||
shortName: 'Urban Switch',
|
shortName: "Urban Switch",
|
||||||
cui: '',
|
cui: "",
|
||||||
color: '#6366f1',
|
color: "#6366f1",
|
||||||
address: '',
|
address: "",
|
||||||
city: 'Cluj-Napoca',
|
city: "Cluj-Napoca",
|
||||||
logo: {
|
logo: {
|
||||||
light: '/logos/logo-us-light.svg',
|
light: "/logos/logo-us-light.svg",
|
||||||
dark: '/logos/logo-us-dark.svg',
|
dark: "/logos/logo-us-light.svg",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
'studii-de-teren': {
|
"studii-de-teren": {
|
||||||
id: 'studii-de-teren',
|
id: "studii-de-teren",
|
||||||
name: 'Studii de Teren SRL',
|
name: "Studii de Teren SRL",
|
||||||
shortName: 'Studii de Teren',
|
shortName: "Studii de Teren",
|
||||||
cui: '',
|
cui: "",
|
||||||
color: '#f59e0b',
|
color: "#f59e0b",
|
||||||
address: '',
|
address: "",
|
||||||
city: 'Cluj-Napoca',
|
city: "Cluj-Napoca",
|
||||||
logo: {
|
logo: {
|
||||||
light: '/logos/logo-sdt-dark.svg',
|
light: "/logos/logo-sdt-light.svg",
|
||||||
dark: '/logos/logo-sdt-light.svg',
|
dark: "/logos/logo-sdt-light.svg",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
group: {
|
group: {
|
||||||
id: 'group',
|
id: "group",
|
||||||
name: 'Grup Companii',
|
name: "Grup Companii",
|
||||||
shortName: 'Grup',
|
shortName: "Grup",
|
||||||
cui: '',
|
cui: "",
|
||||||
color: '#64748b',
|
color: "#64748b",
|
||||||
address: '',
|
address: "",
|
||||||
city: 'Cluj-Napoca',
|
city: "Cluj-Napoca",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,18 +1,38 @@
|
|||||||
'use client';
|
"use client";
|
||||||
|
|
||||||
import type { CompanyId } from '@/core/auth/types';
|
import type { CompanyId } from "@/core/auth/types";
|
||||||
import type { SignatureConfig, SignatureColors, SignatureLayout, SignatureVariant } from '../types';
|
import type {
|
||||||
import { COMPANY_BRANDING, BELETAGE_ADDRESSES, US_ADDRESSES, SDT_ADDRESSES } from '../services/company-branding';
|
SignatureConfig,
|
||||||
import { Input } from '@/shared/components/ui/input';
|
SignatureColors,
|
||||||
import { Label } from '@/shared/components/ui/label';
|
SignatureLayout,
|
||||||
import { Switch } from '@/shared/components/ui/switch';
|
SignatureVariant,
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/shared/components/ui/select';
|
} from "../types";
|
||||||
import { Separator } from '@/shared/components/ui/separator';
|
import {
|
||||||
import { cn } from '@/shared/lib/utils';
|
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 {
|
interface SignatureConfiguratorProps {
|
||||||
config: SignatureConfig;
|
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;
|
onUpdateColor: (key: keyof SignatureColors, value: string) => void;
|
||||||
onUpdateLayout: (key: keyof SignatureLayout, value: number) => void;
|
onUpdateLayout: (key: keyof SignatureLayout, value: number) => void;
|
||||||
onSetVariant: (variant: SignatureVariant) => void;
|
onSetVariant: (variant: SignatureVariant) => void;
|
||||||
@@ -23,58 +43,71 @@ interface SignatureConfiguratorProps {
|
|||||||
/** Color palette per company */
|
/** Color palette per company */
|
||||||
const COMPANY_PALETTES: Record<CompanyId, Record<string, string>> = {
|
const COMPANY_PALETTES: Record<CompanyId, Record<string, string>> = {
|
||||||
beletage: {
|
beletage: {
|
||||||
verde: '#22B5AB',
|
verde: "#22B5AB",
|
||||||
griInchis: '#54504F',
|
griInchis: "#54504F",
|
||||||
griDeschis: '#A7A9AA',
|
griDeschis: "#A7A9AA",
|
||||||
negru: '#323232',
|
negru: "#323232",
|
||||||
},
|
},
|
||||||
'urban-switch': {
|
"urban-switch": {
|
||||||
indigo: '#6366f1',
|
albastru: "#345476",
|
||||||
violet: '#4F46E5',
|
griInchis: "#2D2D2D",
|
||||||
griInchis: '#2D2D2D',
|
griDeschis: "#6B7280",
|
||||||
griDeschis: '#6B7280',
|
negru: "#1F2937",
|
||||||
albastru: '#3B82F6',
|
|
||||||
negru: '#1F2937',
|
|
||||||
},
|
},
|
||||||
'studii-de-teren': {
|
"studii-de-teren": {
|
||||||
amber: '#f59e0b',
|
teal: "#0182A1",
|
||||||
portocaliu: '#D97706',
|
bleumarin: "#000D1A",
|
||||||
griInchis: '#2D2D2D',
|
griInchis: "#2D2D2D",
|
||||||
griDeschis: '#6B7280',
|
griDeschis: "#6B7280",
|
||||||
maro: '#92400E',
|
negru: "#1F2937",
|
||||||
negru: '#1F2937',
|
|
||||||
},
|
},
|
||||||
group: {
|
group: {
|
||||||
gri: '#64748b',
|
gri: "#64748b",
|
||||||
griInchis: '#334155',
|
griInchis: "#334155",
|
||||||
griDeschis: '#94a3b8',
|
griDeschis: "#94a3b8",
|
||||||
negru: '#1e293b',
|
negru: "#1e293b",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const COLOR_LABELS: Record<keyof SignatureColors, string> = {
|
const COLOR_LABELS: Record<keyof SignatureColors, string> = {
|
||||||
prefix: 'Titulatură',
|
prefix: "Titulatură",
|
||||||
name: 'Nume',
|
name: "Nume",
|
||||||
title: 'Funcție',
|
title: "Funcție",
|
||||||
address: 'Adresă',
|
address: "Adresă",
|
||||||
phone: 'Telefon',
|
phone: "Telefon",
|
||||||
website: 'Website',
|
website: "Website",
|
||||||
motto: 'Motto',
|
motto: "Motto",
|
||||||
};
|
};
|
||||||
|
|
||||||
const LAYOUT_CONTROLS: { key: keyof SignatureLayout; label: string; min: number; max: number }[] = [
|
const LAYOUT_CONTROLS: {
|
||||||
{ key: 'greenLineWidth', label: 'Lungime linie accent', min: 50, max: 300 },
|
key: keyof SignatureLayout;
|
||||||
{ key: 'sectionSpacing', label: 'Spațiere secțiuni', min: 0, max: 30 },
|
label: string;
|
||||||
{ key: 'logoSpacing', label: 'Spațiere logo', min: 0, max: 30 },
|
min: number;
|
||||||
{ key: 'titleSpacing', label: 'Spațiere funcție', min: 0, max: 20 },
|
max: number;
|
||||||
{ key: 'gutterWidth', label: 'Aliniere contact', min: 0, max: 150 },
|
}[] = [
|
||||||
{ key: 'iconTextSpacing', label: 'Spațiu icon-text', min: -10, max: 30 },
|
{ key: "greenLineWidth", label: "Lungime linie accent", min: 50, max: 300 },
|
||||||
{ key: 'iconVerticalOffset', label: 'Aliniere verticală iconițe', min: -10, max: 10 },
|
{ key: "sectionSpacing", label: "Spațiere secțiuni", min: 0, max: 30 },
|
||||||
{ key: 'mottoSpacing', label: 'Spațiere motto', min: 0, max: 20 },
|
{ 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({
|
export function SignatureConfigurator({
|
||||||
config, onUpdateField, onUpdateColor, onUpdateLayout, onSetVariant, onSetCompany, onSetAddress,
|
config,
|
||||||
|
onUpdateField,
|
||||||
|
onUpdateColor,
|
||||||
|
onUpdateLayout,
|
||||||
|
onSetVariant,
|
||||||
|
onSetCompany,
|
||||||
|
onSetAddress,
|
||||||
}: SignatureConfiguratorProps) {
|
}: SignatureConfiguratorProps) {
|
||||||
const palette = COMPANY_PALETTES[config.company];
|
const palette = COMPANY_PALETTES[config.company];
|
||||||
|
|
||||||
@@ -83,71 +116,129 @@ export function SignatureConfigurator({
|
|||||||
{/* Company selector */}
|
{/* Company selector */}
|
||||||
<div>
|
<div>
|
||||||
<Label>Companie</Label>
|
<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">
|
<SelectTrigger className="mt-1">
|
||||||
<SelectValue />
|
<SelectValue />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{Object.values(COMPANY_BRANDING).map((b) => (
|
{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>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Address selector (for Beletage) */}
|
{/* Address selector (for Beletage) */}
|
||||||
{config.company === 'beletage' && onSetAddress && (
|
{config.company === "beletage" && onSetAddress && (
|
||||||
<div>
|
<div>
|
||||||
<Label>Adresă birou</Label>
|
<Label>Adresă birou</Label>
|
||||||
<Select
|
<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) => {
|
onValueChange={(v) => {
|
||||||
const key = v as keyof typeof BELETAGE_ADDRESSES;
|
const key = v as AddressKey;
|
||||||
onSetAddress(BELETAGE_ADDRESSES[key]);
|
onSetAddress(BELETAGE_ADDRESSES[key]);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<SelectTrigger className="mt-1"><SelectValue /></SelectTrigger>
|
<SelectTrigger className="mt-1">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="unirii">Str. Unirii, nr. 3</SelectItem>
|
<SelectItem value="christescu">
|
||||||
<SelectItem value="christescu">Str. G-ral Eremia Grigorescu, nr. 21</SelectItem>
|
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>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Address selector (for Urban Switch) */}
|
{/* Address selector (for Urban Switch) */}
|
||||||
{config.company === 'urban-switch' && onSetAddress && (
|
{config.company === "urban-switch" && onSetAddress && (
|
||||||
<div>
|
<div>
|
||||||
<Label>Adresă birou</Label>
|
<Label>Adresă birou</Label>
|
||||||
<Select
|
<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) => {
|
onValueChange={(v) => {
|
||||||
const key = v as keyof typeof US_ADDRESSES;
|
const key = v as AddressKey;
|
||||||
onSetAddress(US_ADDRESSES[key]);
|
onSetAddress(US_ADDRESSES[key]);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<SelectTrigger className="mt-1"><SelectValue /></SelectTrigger>
|
<SelectTrigger className="mt-1">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<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>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Address selector (for Studii de Teren) */}
|
{/* Address selector (for Studii de Teren) */}
|
||||||
{config.company === 'studii-de-teren' && onSetAddress && (
|
{config.company === "studii-de-teren" && onSetAddress && (
|
||||||
<div>
|
<div>
|
||||||
<Label>Adresă birou</Label>
|
<Label>Adresă birou</Label>
|
||||||
<Select
|
<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) => {
|
onValueChange={(v) => {
|
||||||
const key = v as keyof typeof SDT_ADDRESSES;
|
const key = v as AddressKey;
|
||||||
onSetAddress(SDT_ADDRESSES[key]);
|
onSetAddress(SDT_ADDRESSES[key]);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<SelectTrigger className="mt-1"><SelectValue /></SelectTrigger>
|
<SelectTrigger className="mt-1">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<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>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
@@ -160,19 +251,40 @@ export function SignatureConfigurator({
|
|||||||
<h3 className="text-sm font-semibold">Date personale</h3>
|
<h3 className="text-sm font-semibold">Date personale</h3>
|
||||||
<div>
|
<div>
|
||||||
<Label htmlFor="sig-prefix">Titulatură (prefix)</Label>
|
<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>
|
||||||
<div>
|
<div>
|
||||||
<Label htmlFor="sig-name">Nume și Prenume</Label>
|
<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>
|
||||||
<div>
|
<div>
|
||||||
<Label htmlFor="sig-title">Funcția</Label>
|
<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>
|
||||||
<div>
|
<div>
|
||||||
<Label htmlFor="sig-phone">Telefon (format 07xxxxxxxx)</Label>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -181,19 +293,32 @@ export function SignatureConfigurator({
|
|||||||
{/* Variant */}
|
{/* Variant */}
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<h3 className="text-sm font-semibold">Variantă</h3>
|
<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>
|
<SelectTrigger>
|
||||||
<SelectValue />
|
<SelectValue />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<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="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>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Switch checked={config.useSvg} onCheckedChange={(v) => onUpdateField('useSvg', v)} id="svg-toggle" />
|
<Switch
|
||||||
<Label htmlFor="svg-toggle" className="cursor-pointer text-sm">Imagini SVG (calitate maximă)</Label>
|
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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -202,9 +327,12 @@ export function SignatureConfigurator({
|
|||||||
{/* Colors — company-specific palette */}
|
{/* Colors — company-specific palette */}
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<h3 className="text-sm font-semibold">Culori text</h3>
|
<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">
|
<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">
|
<div className="flex gap-1.5">
|
||||||
{Object.values(palette).map((color) => (
|
{Object.values(palette).map((color) => (
|
||||||
<button
|
<button
|
||||||
@@ -212,17 +340,18 @@ export function SignatureConfigurator({
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={() => onUpdateColor(colorKey, color)}
|
onClick={() => onUpdateColor(colorKey, color)}
|
||||||
className={cn(
|
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
|
config.colors[colorKey] === color
|
||||||
? 'border-primary scale-110 ring-2 ring-primary/30'
|
? "border-primary scale-110 ring-2 ring-primary/30"
|
||||||
: 'border-transparent hover:scale-105'
|
: "border-transparent hover:scale-105",
|
||||||
)}
|
)}
|
||||||
style={{ backgroundColor: color }}
|
style={{ backgroundColor: color }}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
),
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Separator />
|
<Separator />
|
||||||
@@ -234,14 +363,18 @@ export function SignatureConfigurator({
|
|||||||
<div key={key}>
|
<div key={key}>
|
||||||
<div className="flex justify-between text-sm">
|
<div className="flex justify-between text-sm">
|
||||||
<Label>{label}</Label>
|
<Label>{label}</Label>
|
||||||
<span className="text-muted-foreground">{config.layout[key]}px</span>
|
<span className="text-muted-foreground">
|
||||||
|
{config.layout[key]}px
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input
|
||||||
type="range"
|
type="range"
|
||||||
min={min}
|
min={min}
|
||||||
max={max}
|
max={max}
|
||||||
value={config.layout[key]}
|
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"
|
className="mt-1 w-full accent-primary"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,132 +1,150 @@
|
|||||||
import type { CompanyId } from '@/core/auth/types';
|
import type { CompanyId } from "@/core/auth/types";
|
||||||
import type { CompanyBranding, SignatureColors } from '../types';
|
import type { CompanyBranding, SignatureColors } from "../types";
|
||||||
|
|
||||||
const BELETAGE_COLORS: SignatureColors = {
|
const BELETAGE_COLORS: SignatureColors = {
|
||||||
prefix: '#54504F',
|
prefix: "#54504F",
|
||||||
name: '#54504F',
|
name: "#54504F",
|
||||||
title: '#A7A9AA',
|
title: "#A7A9AA",
|
||||||
address: '#A7A9AA',
|
address: "#A7A9AA",
|
||||||
phone: '#54504F',
|
phone: "#54504F",
|
||||||
website: '#54504F',
|
website: "#54504F",
|
||||||
motto: '#22B5AB',
|
motto: "#22B5AB",
|
||||||
};
|
};
|
||||||
|
|
||||||
const URBAN_SWITCH_COLORS: SignatureColors = {
|
const URBAN_SWITCH_COLORS: SignatureColors = {
|
||||||
prefix: '#2D2D2D',
|
prefix: "#345476",
|
||||||
name: '#2D2D2D',
|
name: "#345476",
|
||||||
title: '#6B7280',
|
title: "#6B7280",
|
||||||
address: '#6B7280',
|
address: "#6B7280",
|
||||||
phone: '#2D2D2D',
|
phone: "#345476",
|
||||||
website: '#4F46E5',
|
website: "#345476",
|
||||||
motto: '#6366f1',
|
motto: "#345476",
|
||||||
};
|
};
|
||||||
|
|
||||||
const STUDII_COLORS: SignatureColors = {
|
const STUDII_COLORS: SignatureColors = {
|
||||||
prefix: '#2D2D2D',
|
prefix: "#000D1A",
|
||||||
name: '#2D2D2D',
|
name: "#000D1A",
|
||||||
title: '#6B7280',
|
title: "#6B7280",
|
||||||
address: '#6B7280',
|
address: "#6B7280",
|
||||||
phone: '#2D2D2D',
|
phone: "#000D1A",
|
||||||
website: '#D97706',
|
website: "#0182A1",
|
||||||
motto: '#f59e0b',
|
motto: "#0182A1",
|
||||||
};
|
};
|
||||||
|
|
||||||
const ADDR_UNIRII = ['str. Unirii, nr. 3, ap. 26', 'Cluj-Napoca, Cluj 400417', 'România'] as const;
|
const ADDR_CHRISTESCU = [
|
||||||
const ADDR_CHRISTESCU = ['str. G-ral Eremia Grigorescu, nr. 21', 'Cluj-Napoca, Cluj 400304', 'România'] as const;
|
"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) */
|
/** Available address options for Beletage (toggle between offices) */
|
||||||
export const BELETAGE_ADDRESSES: { unirii: string[]; christescu: string[] } = {
|
export const BELETAGE_ADDRESSES: Record<AddressKey, string[]> = {
|
||||||
unirii: [...ADDR_UNIRII],
|
|
||||||
christescu: [...ADDR_CHRISTESCU],
|
christescu: [...ADDR_CHRISTESCU],
|
||||||
|
unirii: [...ADDR_UNIRII],
|
||||||
|
albac: [...ADDR_ALBAC],
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Available address options for Urban Switch */
|
/** Available address options for Urban Switch */
|
||||||
export const US_ADDRESSES: { unirii: string[] } = {
|
export const US_ADDRESSES: Record<AddressKey, string[]> = {
|
||||||
unirii: ['str. Unirii, nr. 3, ap. 26', 'Cluj-Napoca, Cluj 400417', 'România'],
|
christescu: [...ADDR_CHRISTESCU],
|
||||||
|
unirii: [...ADDR_UNIRII],
|
||||||
|
albac: [...ADDR_ALBAC],
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Available address options for Studii de Teren */
|
/** Available address options for Studii de Teren */
|
||||||
export const SDT_ADDRESSES: { unirii: string[] } = {
|
export const SDT_ADDRESSES: Record<AddressKey, string[]> = {
|
||||||
unirii: ['str. Unirii, nr. 3, ap. 26', 'Cluj-Napoca, Cluj 400417', 'românia'],
|
christescu: [...ADDR_CHRISTESCU],
|
||||||
|
unirii: [...ADDR_UNIRII],
|
||||||
|
albac: [...ADDR_ALBAC],
|
||||||
};
|
};
|
||||||
|
|
||||||
export const COMPANY_BRANDING: Record<CompanyId, CompanyBranding> = {
|
export const COMPANY_BRANDING: Record<CompanyId, CompanyBranding> = {
|
||||||
beletage: {
|
beletage: {
|
||||||
id: 'beletage',
|
id: "beletage",
|
||||||
name: 'Beletage SRL',
|
name: "Beletage SRL",
|
||||||
accent: '#22B5AB',
|
accent: "#22B5AB",
|
||||||
logo: {
|
logo: {
|
||||||
png: 'https://beletage.ro/img/Semnatura-Logo.png',
|
png: "https://beletage.ro/img/Semnatura-Logo.png",
|
||||||
svg: 'https://beletage.ro/img/Logo-Beletage.svg',
|
svg: "https://beletage.ro/img/Logo-Beletage.svg",
|
||||||
},
|
},
|
||||||
slashGrey: {
|
slashGrey: {
|
||||||
png: 'https://beletage.ro/img/Grey-slash.png',
|
png: "https://beletage.ro/img/Grey-slash.png",
|
||||||
svg: 'https://beletage.ro/img/Grey-slash.svg',
|
svg: "https://beletage.ro/img/Grey-slash.svg",
|
||||||
},
|
},
|
||||||
slashAccent: {
|
slashAccent: {
|
||||||
png: 'https://beletage.ro/img/Green-slash.png',
|
png: "https://beletage.ro/img/Green-slash.png",
|
||||||
svg: 'https://beletage.ro/img/Green-slash.svg',
|
svg: "https://beletage.ro/img/Green-slash.svg",
|
||||||
},
|
},
|
||||||
address: [...ADDR_UNIRII],
|
logoDimensions: { width: 162, height: 24 },
|
||||||
website: 'www.beletage.ro',
|
address: [...ADDR_CHRISTESCU],
|
||||||
motto: 'we make complex simple',
|
website: "www.beletage.ro",
|
||||||
|
motto: "we make complex simple",
|
||||||
defaultColors: BELETAGE_COLORS,
|
defaultColors: BELETAGE_COLORS,
|
||||||
},
|
},
|
||||||
'urban-switch': {
|
"urban-switch": {
|
||||||
id: 'urban-switch',
|
id: "urban-switch",
|
||||||
name: 'Urban Switch SRL',
|
name: "Urban Switch SRL",
|
||||||
accent: '#6366f1',
|
accent: "#345476",
|
||||||
logo: {
|
logo: {
|
||||||
png: '/logos/logo-us-dark.svg',
|
png: "/logos/logo-us-light.svg",
|
||||||
svg: '/logos/logo-us-dark.svg',
|
svg: "/logos/logo-us-light.svg",
|
||||||
},
|
},
|
||||||
slashGrey: {
|
slashGrey: {
|
||||||
png: 'https://beletage.ro/img/Grey-slash.png',
|
png: "https://beletage.ro/img/Grey-slash.png",
|
||||||
svg: 'https://beletage.ro/img/Grey-slash.svg',
|
svg: "https://beletage.ro/img/Grey-slash.svg",
|
||||||
},
|
},
|
||||||
slashAccent: {
|
slashAccent: { png: "", svg: "" },
|
||||||
png: '/logos/logo-us-light.svg',
|
logoDimensions: { width: 140, height: 24 },
|
||||||
svg: '/logos/logo-us-light.svg',
|
address: [...ADDR_CHRISTESCU],
|
||||||
},
|
website: "www.urbanswitch.ro",
|
||||||
address: ['str. Unirii, nr. 3, ap. 26', 'Cluj-Napoca, Cluj 400417', 'România'],
|
motto: "shaping urban futures",
|
||||||
website: 'www.urbanswitch.ro',
|
|
||||||
motto: 'shaping urban futures',
|
|
||||||
defaultColors: URBAN_SWITCH_COLORS,
|
defaultColors: URBAN_SWITCH_COLORS,
|
||||||
},
|
},
|
||||||
'studii-de-teren': {
|
"studii-de-teren": {
|
||||||
id: 'studii-de-teren',
|
id: "studii-de-teren",
|
||||||
name: 'Studii de Teren SRL',
|
name: "Studii de Teren SRL",
|
||||||
accent: '#f59e0b',
|
accent: "#0182A1",
|
||||||
logo: {
|
logo: {
|
||||||
png: '/logos/logo-sdt-dark.svg',
|
png: "/logos/logo-sdt-light.svg",
|
||||||
svg: '/logos/logo-sdt-dark.svg',
|
svg: "/logos/logo-sdt-light.svg",
|
||||||
},
|
},
|
||||||
slashGrey: {
|
slashGrey: {
|
||||||
png: 'https://beletage.ro/img/Grey-slash.png',
|
png: "https://beletage.ro/img/Grey-slash.png",
|
||||||
svg: 'https://beletage.ro/img/Grey-slash.svg',
|
svg: "https://beletage.ro/img/Grey-slash.svg",
|
||||||
},
|
},
|
||||||
slashAccent: {
|
slashAccent: { png: "", svg: "" },
|
||||||
png: '/logos/logo-sdt-light.svg',
|
logoDimensions: { width: 71, height: 24 },
|
||||||
svg: '/logos/logo-sdt-light.svg',
|
address: [...ADDR_CHRISTESCU],
|
||||||
},
|
website: "www.studiideteren.ro",
|
||||||
address: ['str. Unirii, nr. 3, ap. 26', 'Cluj-Napoca, Cluj 400417', 'România'],
|
motto: "ground truth, measured right",
|
||||||
website: 'www.studiideteren.ro',
|
|
||||||
motto: 'ground truth, measured right',
|
|
||||||
defaultColors: STUDII_COLORS,
|
defaultColors: STUDII_COLORS,
|
||||||
},
|
},
|
||||||
group: {
|
group: {
|
||||||
id: 'group',
|
id: "group",
|
||||||
name: 'Grup Companii',
|
name: "Grup Companii",
|
||||||
accent: '#64748b',
|
accent: "#64748b",
|
||||||
logo: { png: '', svg: '' },
|
logo: { png: "", svg: "" },
|
||||||
slashGrey: {
|
slashGrey: {
|
||||||
png: 'https://beletage.ro/img/Grey-slash.png',
|
png: "https://beletage.ro/img/Grey-slash.png",
|
||||||
svg: 'https://beletage.ro/img/Grey-slash.svg',
|
svg: "https://beletage.ro/img/Grey-slash.svg",
|
||||||
},
|
},
|
||||||
slashAccent: { png: '', svg: '' },
|
slashAccent: { png: "", svg: "" },
|
||||||
address: ['Cluj-Napoca, Cluj', 'România'],
|
address: ["Cluj-Napoca, Cluj", "România"],
|
||||||
website: '',
|
website: "",
|
||||||
motto: '',
|
motto: "",
|
||||||
defaultColors: BELETAGE_COLORS,
|
defaultColors: BELETAGE_COLORS,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import type { SignatureConfig, CompanyBranding } from '../types';
|
import type { SignatureConfig, CompanyBranding } from "../types";
|
||||||
import { getBranding } from './company-branding';
|
import { getBranding } from "./company-branding";
|
||||||
|
|
||||||
export function formatPhone(raw: string): { display: string; link: string } {
|
export function formatPhone(raw: string): { display: string; link: string } {
|
||||||
const clean = raw.replace(/\s/g, '');
|
const clean = raw.replace(/\s/g, "");
|
||||||
if (clean.length === 10 && clean.startsWith('07')) {
|
if (clean.length === 10 && clean.startsWith("07")) {
|
||||||
return {
|
return {
|
||||||
display: `+40 ${clean.substring(1, 4)} ${clean.substring(4, 7)} ${clean.substring(7, 10)}`,
|
display: `+40 ${clean.substring(1, 4)} ${clean.substring(4, 7)} ${clean.substring(7, 10)}`,
|
||||||
link: `tel:+40${clean.substring(1)}`,
|
link: `tel:+40${clean.substring(1)}`,
|
||||||
@@ -17,30 +17,47 @@ export function generateSignatureHtml(config: SignatureConfig): string {
|
|||||||
const address = config.addressOverride ?? branding.address;
|
const address = config.addressOverride ?? branding.address;
|
||||||
const { display: phone, link: phoneLink } = formatPhone(config.phone);
|
const { display: phone, link: phoneLink } = formatPhone(config.phone);
|
||||||
const images = config.useSvg
|
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 {
|
const {
|
||||||
greenLineWidth, gutterWidth, iconTextSpacing, iconVerticalOffset,
|
greenLineWidth,
|
||||||
mottoSpacing, sectionSpacing, titleSpacing, logoSpacing,
|
gutterWidth,
|
||||||
|
iconTextSpacing,
|
||||||
|
iconVerticalOffset,
|
||||||
|
mottoSpacing,
|
||||||
|
sectionSpacing,
|
||||||
|
titleSpacing,
|
||||||
|
logoSpacing,
|
||||||
} = config.layout;
|
} = config.layout;
|
||||||
const colors = config.colors;
|
const colors = config.colors;
|
||||||
|
|
||||||
const isReply = config.variant === 'reply' || config.variant === 'minimal';
|
const isReply = config.variant === "reply" || config.variant === "minimal";
|
||||||
const isMinimal = 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 logoDim = branding.logoDimensions ?? { width: 162, height: 24 };
|
||||||
const hideTitle = isReply ? hide : '';
|
|
||||||
const hideLogo = isReply ? hide : '';
|
const hide =
|
||||||
const hideBottom = isMinimal ? hide : '';
|
"mso-hide:all;display:none!important;max-height:0;overflow:hidden;font-size:0;line-height:0;";
|
||||||
const hidePhoneIcon = isMinimal ? hide : '';
|
const hideTitle = isReply ? hide : "";
|
||||||
|
const hideLogo = isReply ? hide : "";
|
||||||
|
const hideBottom = isMinimal ? hide : "";
|
||||||
|
const hidePhoneIcon = isMinimal ? hide : "";
|
||||||
|
|
||||||
const spacerWidth = Math.max(0, iconTextSpacing);
|
const spacerWidth = Math.max(0, iconTextSpacing);
|
||||||
const textPaddingLeft = Math.max(0, -iconTextSpacing);
|
const textPaddingLeft = Math.max(0, -iconTextSpacing);
|
||||||
|
|
||||||
const prefixHtml = config.prefix
|
const prefixHtml = config.prefix
|
||||||
? `<span style="font-size:13px; color:${colors.prefix};">${esc(config.prefix)} </span>`
|
? `<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;">
|
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>
|
<tbody>
|
||||||
@@ -57,28 +74,32 @@ export function generateSignatureHtml(config: SignatureConfig): string {
|
|||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr style="${hideLogo}"><td style="padding:${logoSpacing}px 0 ${logoSpacing + 2}px 0;">
|
<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">
|
images.logo
|
||||||
</a>` : ''}
|
? `<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>
|
</td></tr>
|
||||||
<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;">
|
<table role="presentation" cellpadding="0" cellspacing="0" border="0" width="540" style="font-size:13px; line-height:18px;">
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr style="${hideLogo}">
|
<tr style="${hideLogo}">
|
||||||
<td width="${gutterWidth}" style="width:${gutterWidth}px; font-size:0; line-height:0;"></td>
|
<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;">
|
<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>
|
||||||
<td width="${spacerWidth}" style="width:${spacerWidth}px; font-size:0; line-height: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;">
|
<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>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td width="${gutterWidth}" style="width:${gutterWidth}px; font-size:0; line-height:0;"></td>
|
<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}">
|
<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>
|
||||||
<td width="${isMinimal ? 0 : spacerWidth}" style="width:${isMinimal ? 0 : spacerWidth}px; font-size:0; line-height: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;">
|
<td style="vertical-align:top; padding:8px 0 0 ${isMinimal ? 0 : textPaddingLeft}px;">
|
||||||
@@ -89,7 +110,7 @@ export function generateSignatureHtml(config: SignatureConfig): string {
|
|||||||
</table>
|
</table>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</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}">
|
<tr style="${hideBottom}">
|
||||||
<td style="padding:0; font-size:0; line-height:0;">
|
<td style="padding:0; font-size:0; line-height:0;">
|
||||||
<table role="presentation" cellpadding="0" cellspacing="0" border="0" width="540">
|
<table role="presentation" cellpadding="0" cellspacing="0" border="0" width="540">
|
||||||
@@ -100,22 +121,22 @@ export function generateSignatureHtml(config: SignatureConfig): string {
|
|||||||
</table>
|
</table>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</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>
|
</tbody>
|
||||||
</table>`;
|
</table>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function esc(text: string): string {
|
function esc(text: string): string {
|
||||||
return text
|
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 {
|
export function downloadSignatureHtml(html: string, filename: string): void {
|
||||||
const blob = new Blob([html], { type: 'text/html' });
|
const blob = new Blob([html], { type: "text/html" });
|
||||||
const a = document.createElement('a');
|
const a = document.createElement("a");
|
||||||
a.href = URL.createObjectURL(blob);
|
a.href = URL.createObjectURL(blob);
|
||||||
a.download = filename;
|
a.download = filename;
|
||||||
document.body.appendChild(a);
|
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 {
|
export interface SignatureColors {
|
||||||
prefix: string;
|
prefix: string;
|
||||||
@@ -30,6 +30,8 @@ export interface CompanyBranding {
|
|||||||
logo: { png: string; svg: string };
|
logo: { png: string; svg: string };
|
||||||
slashGrey: { png: string; svg: string };
|
slashGrey: { png: string; svg: string };
|
||||||
slashAccent: { png: string; svg: string };
|
slashAccent: { png: string; svg: string };
|
||||||
|
/** Logo dimensions (width × height) for the signature HTML */
|
||||||
|
logoDimensions?: { width: number; height: number };
|
||||||
address: string[];
|
address: string[];
|
||||||
website: string;
|
website: string;
|
||||||
motto: string;
|
motto: string;
|
||||||
|
|||||||
@@ -1,26 +1,50 @@
|
|||||||
'use client';
|
"use client";
|
||||||
|
|
||||||
import Image from 'next/image';
|
import Image from "next/image";
|
||||||
import Link from 'next/link';
|
import Link from "next/link";
|
||||||
import { usePathname } from 'next/navigation';
|
import { usePathname } from "next/navigation";
|
||||||
import { useTheme } from 'next-themes';
|
import { useMemo } from "react";
|
||||||
import { useMemo } from 'react';
|
import * as Icons from "lucide-react";
|
||||||
import * as Icons from 'lucide-react';
|
import { buildNavigation } from "@/config/navigation";
|
||||||
import { buildNavigation } from '@/config/navigation';
|
import { COMPANIES } from "@/config/companies";
|
||||||
import { COMPANIES } from '@/config/companies';
|
import { useFeatureFlag } from "@/core/feature-flags";
|
||||||
import { useFeatureFlag } from '@/core/feature-flags';
|
import { cn } from "@/shared/lib/utils";
|
||||||
import { cn } from '@/shared/lib/utils';
|
import { ScrollArea } from "@/shared/components/ui/scroll-area";
|
||||||
import { ScrollArea } from '@/shared/components/ui/scroll-area';
|
import { Separator } from "@/shared/components/ui/separator";
|
||||||
import { Separator } from '@/shared/components/ui/separator';
|
|
||||||
|
|
||||||
function DynamicIcon({ name, className }: { name: string; className?: string }) {
|
function DynamicIcon({
|
||||||
const pascalName = name.replace(/(^|-)([a-z])/g, (_, _p, c: string) => c.toUpperCase());
|
name,
|
||||||
const IconComponent = (Icons as unknown as Record<string, React.ComponentType<{ className?: string }>>)[pascalName];
|
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} />;
|
if (!IconComponent) return <Icons.Circle className={className} />;
|
||||||
return <IconComponent 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);
|
const enabled = useFeatureFlag(item.featureFlag);
|
||||||
if (!enabled) return null;
|
if (!enabled) return null;
|
||||||
|
|
||||||
@@ -28,10 +52,10 @@ function NavItem({ item, isActive }: { item: { id: string; label: string; icon:
|
|||||||
<Link
|
<Link
|
||||||
href={item.href}
|
href={item.href}
|
||||||
className={cn(
|
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
|
isActive
|
||||||
? 'bg-accent text-accent-foreground font-medium'
|
? "bg-accent text-accent-foreground font-medium"
|
||||||
: 'text-muted-foreground hover:bg-accent/50 hover:text-accent-foreground'
|
: "text-muted-foreground hover:bg-accent/50 hover:text-accent-foreground",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<DynamicIcon name={item.icon} className="h-4 w-4 shrink-0" />
|
<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() {
|
function SidebarLogo() {
|
||||||
const { resolvedTheme } = useTheme();
|
const sdt = COMPANIES["studii-de-teren"];
|
||||||
const sdt = COMPANIES['studii-de-teren'];
|
|
||||||
const logoSrc = sdt.logo
|
const logoSrc = sdt.logo?.light ?? null;
|
||||||
? (resolvedTheme === 'dark' ? sdt.logo.dark : sdt.logo.light)
|
|
||||||
: null;
|
|
||||||
|
|
||||||
if (!logoSrc) {
|
if (!logoSrc) {
|
||||||
return <Icons.LayoutDashboard className="h-5 w-5 text-primary" />;
|
return <Icons.LayoutDashboard className="h-5 w-5 text-primary" />;
|
||||||
@@ -58,6 +80,7 @@ function SidebarLogo() {
|
|||||||
width={28}
|
width={28}
|
||||||
height={28}
|
height={28}
|
||||||
className="h-7 w-7 shrink-0"
|
className="h-7 w-7 shrink-0"
|
||||||
|
suppressHydrationWarning
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -77,10 +100,10 @@ export function Sidebar() {
|
|||||||
<Link
|
<Link
|
||||||
href="/"
|
href="/"
|
||||||
className={cn(
|
className={cn(
|
||||||
'mb-1 flex items-center gap-3 rounded-md px-3 py-2 text-sm transition-colors',
|
"mb-1 flex items-center gap-3 rounded-md px-3 py-2 text-sm transition-colors",
|
||||||
pathname === '/'
|
pathname === "/"
|
||||||
? 'bg-accent text-accent-foreground font-medium'
|
? "bg-accent text-accent-foreground font-medium"
|
||||||
: 'text-muted-foreground hover:bg-accent/50 hover:text-accent-foreground'
|
: "text-muted-foreground hover:bg-accent/50 hover:text-accent-foreground",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<Icons.Home className="h-4 w-4" />
|
<Icons.Home className="h-4 w-4" />
|
||||||
|
|||||||
Reference in New Issue
Block a user