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

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

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

View File

@@ -19,7 +19,7 @@
## AI Model Recommendations ## 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,10 +163,12 @@
### 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,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. **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

View File

@@ -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

View File

@@ -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",
}, },
}; };

View File

@@ -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>

View File

@@ -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,
}, },
}; };

View File

@@ -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, '&amp;') .replace(/&/g, "&amp;")
.replace(/</g, '&lt;') .replace(/</g, "&lt;")
.replace(/>/g, '&gt;') .replace(/>/g, "&gt;")
.replace(/"/g, '&quot;'); .replace(/"/g, "&quot;");
} }
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);

View File

@@ -1,6 +1,6 @@
import type { CompanyId } from '@/core/auth/types'; import type { CompanyId } from "@/core/auth/types";
export type SignatureVariant = 'full' | 'reply' | 'minimal'; export type SignatureVariant = "full" | "reply" | "minimal";
export interface SignatureColors { 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;

View File

@@ -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" />