diff --git a/ROADMAP.md b/ROADMAP.md index 1c8f6ea..ddc7b56 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -18,11 +18,11 @@ ## AI Model Recommendations -| Tag | Claude | OpenAI | Google | Best For | -|---|---|---|---|---| -| `[HEAVY]` | Opus 4.6 | GPT-5.3-Codex | Gemini 3 Pro | Complex multi-file features, business logic, architecture, new modules | -| `[STANDARD]` | Sonnet 4.6 | GPT-5.2 | Gemini 3 Flash | Refactoring, moderate features, UI work, tests, documentation | -| `[LIGHT]` | Haiku 4.5 | GPT-4o-mini | Gemini 2.5 Flash | Quick fixes, small edits, config changes, build debugging | +| Tag | Claude | OpenAI | Google | Best For | +| ------------ | ---------- | ------------- | ---------------- | ---------------------------------------------------------------------- | +| `[HEAVY]` | Opus 4.6 | GPT-5.3-Codex | Gemini 3 Pro | Complex multi-file features, business logic, architecture, new modules | +| `[STANDARD]` | Sonnet 4.6 | GPT-5.2 | Gemini 3 Flash | Refactoring, moderate features, UI work, tests, documentation | +| `[LIGHT]` | Haiku 4.5 | GPT-4o-mini | Gemini 2.5 Flash | Quick fixes, small edits, config changes, build debugging | **Default recommendation: Sonnet 4.6** — it matches Opus-class performance at Sonnet pricing ($3/$15 per M tokens). Use Opus only for tasks marked `[HEAVY]`. Use Haiku for tasks marked `[LIGHT]`. @@ -30,22 +30,22 @@ ## Current Module Status vs. XLSX Spec -| # | Module | Core Done | Gaps Remaining | New Features Needed | -|---|---|---|---|---| -| 1 | Registratura | YES | Linked-entry selector capped at 20 | Workflow automation, email integration, OCR | -| 2 | Email Signature | YES | US/SDT logo files may be missing from `/public/logos/`; US/SDT no address toggle | AD sync, branding packs | -| 3 | Word XML | YES | POT/CUT toggle exists (spec says remove) | Schema validator, visual mapper | -| 4 | Digital Signatures | YES | No file upload (URL only); tags not editable in form | Permission layers, document insertion | -| 5 | Password Vault | YES | Unencrypted storage; no strength meter; no company scope | Hardware key, rotation reminders | -| 6 | IT Inventory | YES | assignedTo not linked to contacts; no maintenance log | Network scan import | -| 7 | Address Book | YES | No vCard export; no reverse Registratura lookup | Email sync, deduplication | -| 8 | Prompt Generator | YES | Missing architecture viz templates (sketch→render, photorealism) | Prompt scoring | -| 9 | Word Templates | YES | No clause library; placeholders manual only; no Word generation | Diff compare, document generator | -| 10 | Tag Manager | YES | No US/SDT project seeds; no mandatory-category enforcement | Server tag sync, smart suggestions | -| 11 | Mini Utilities | PARTIAL | Missing: U→R value, AI artifact cleaner, MDLPA validator, PDF reducer, OCR | More converters | -| 12 | Dashboard | BASIC | No activity feed, no notifications, no KPI panels | Custom dashboards per role | -| 13 | AI Chat | DEMO ONLY | No API integration, no key config, no streaming | Conversation templates | -| 14 | Hot Desk | NOT STARTED | Entire module missing | — | +| # | Module | Core Done | Gaps Remaining | New Features Needed | +| --- | ------------------ | ----------- | -------------------------------------------------------------------------------- | ------------------------------------------- | +| 1 | Registratura | YES | Linked-entry selector capped at 20 | Workflow automation, email integration, OCR | +| 2 | Email Signature | YES | US/SDT logo files may be missing from `/public/logos/`; US/SDT no address toggle | AD sync, branding packs | +| 3 | Word XML | YES | POT/CUT toggle exists (spec says remove) | Schema validator, visual mapper | +| 4 | Digital Signatures | YES | No file upload (URL only); tags not editable in form | Permission layers, document insertion | +| 5 | Password Vault | YES | Unencrypted storage; no strength meter; no company scope | Hardware key, rotation reminders | +| 6 | IT Inventory | YES | assignedTo not linked to contacts; no maintenance log | Network scan import | +| 7 | Address Book | YES | No vCard export; no reverse Registratura lookup | Email sync, deduplication | +| 8 | Prompt Generator | YES | Missing architecture viz templates (sketch→render, photorealism) | Prompt scoring | +| 9 | Word Templates | YES | No clause library; placeholders manual only; no Word generation | Diff compare, document generator | +| 10 | Tag Manager | YES | No US/SDT project seeds; no mandatory-category enforcement | Server tag sync, smart suggestions | +| 11 | Mini Utilities | PARTIAL | Missing: U→R value, AI artifact cleaner, MDLPA validator, PDF reducer, OCR | More converters | +| 12 | Dashboard | BASIC | No activity feed, no notifications, no KPI panels | Custom dashboards per role | +| 13 | AI Chat | DEMO ONLY | No API integration, no key config, no streaming | Conversation templates | +| 14 | Hot Desk | NOT STARTED | Entire module missing | — | --- @@ -75,6 +75,7 @@ ### 1.03 `[STANDARD]` Prompt Generator — Architecture Visualization Templates **What:** Add 6+ new builtin templates per xlsx spec: + 1. Architectural rendering prompt (basic massing to detailed) 2. Sketch → professional render prompt 3. Visualization refinement prompt (photorealism fine-tuning) @@ -94,11 +95,13 @@ ### 1.04 `[STANDARD]` Tag Manager — US/SDT Project Seeds + Mandatory Categories **What:** + 1. Add Urban Switch and Studii de Teren project numbering to seed data (US-001, SDT-001 format) 2. Enforce mandatory 1st category (project) and 2nd category (phase) when creating tags — show validation error if missing 3. Import the full tag structure from `legacy/manicprojects/current manic time Tags.txt` in proper 1st→5th category hierarchy **Files to modify:** + - `src/modules/tag-manager/services/seed-data.ts` — Add US/SDT projects - `src/modules/tag-manager/components/tag-create-form.tsx` — Add mandatory validation @@ -107,6 +110,7 @@ ### 1.05 `[STANDARD]` Mini Utilities — Add Missing Tools **What:** Add the 5 missing tools from xlsx: + 1. **U-value → R-value converter** (R = 1/U, with material thickness input) 2. **AI artifact cleaner** (strip markdown formatting, fix encoding, remove prompt artifacts from pasted text) 3. **MDLPA date locale validator** (validate Romanian administrative dates against legal calendar) @@ -121,10 +125,12 @@ ### 1.06 `[STANDARD]` Digital Signatures — File Upload + Tag Editing **What:** + 1. Add drag-and-drop / file picker for uploading signature/stamp images (convert to base64 on upload, like Registratura attachments) 2. Add tag input field to the asset form (tags field exists in type but form doesn't render it) **Files to modify:** + - `src/modules/digital-signatures/components/` — asset form component --- @@ -132,11 +138,13 @@ ### 1.07 `[LIGHT]` Password Vault — Company Scope + Strength Meter **What:** + 1. Add `company` field to credential type and form (scope passwords to a company) 2. Add password strength indicator (visual bar: weak/medium/strong based on length + character diversity) 3. Rename `encryptedPassword` → `password` in the type (it's not encrypted, the name is misleading) **Files to modify:** + - `src/modules/password-vault/types.ts` - `src/modules/password-vault/components/` — form and list components @@ -146,6 +154,7 @@ **What:** Change `assignedTo` from free text to an autocomplete that links to Address Book contacts (same pattern as Registratura sender/recipient). **Files to modify:** + - `src/modules/it-inventory/components/` — equipment form - `src/modules/it-inventory/types.ts` — Add `assignedToContactId?: string` @@ -154,12 +163,14 @@ ### 1.09 `[STANDARD]` Address Book — vCard Export + Registratura Reverse Lookup **What:** + 1. Add "Export vCard" button per contact (generate `.vcf` file download) 2. Add a section showing Registratura entries where this contact appears as sender or recipient **Files to modify:** + - `src/modules/address-book/components/` — contact card/detail view -**Files to create:** + **Files to create:** - `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. **Files to modify:** + - `src/modules/word-templates/components/` — template form -**Files to create:** + **Files to create:** - `src/modules/word-templates/services/placeholder-parser.ts` --- @@ -177,6 +189,7 @@ ### 1.11 `[STANDARD]` Dashboard — Activity Feed + KPI Panels **What:** + 1. Add an activity feed showing recent actions across modules (last 20 creates/updates/deletes from localStorage timestamps) 2. Add KPI cards: entries this week, deadlines this week, overdue count, contacts added this month 3. Wire the `DashboardWidget` type that already exists in `types.ts` @@ -207,6 +220,7 @@ ### 2.01 `[HEAVY]` Hot Desk Module — Full Implementation **What:** Build Module 14 from scratch per xlsx spec: + - 4 desks in a shared room - Users reserve desks 1 week ahead - Calendar view showing desk availability per day @@ -215,6 +229,7 @@ - Visual room layout showing which desks are booked **Module structure:** + ``` src/modules/hot-desk/ ├── components/ @@ -232,6 +247,7 @@ src/modules/hot-desk/ ``` **Files to also create/modify:** + - `src/app/(modules)/hot-desk/page.tsx` — Route - `src/config/modules.ts` — Register module - `src/config/navigation.ts` — Add sidebar entry @@ -248,9 +264,11 @@ src/modules/hot-desk/ ### 3.01 `[STANDARD]` Install Testing Framework (Vitest) **What:** Install and configure Vitest with React Testing Library. + ```bash npm install -D vitest @testing-library/react @testing-library/jest-dom jsdom @vitest/coverage-v8 ``` + **Files to create:** `vitest.config.ts`, `src/test-setup.ts` **Files to modify:** `package.json` (add test scripts) @@ -259,6 +277,7 @@ npm install -D vitest @testing-library/react @testing-library/jest-dom jsdom @vi ### 3.02 `[STANDARD]` Unit Tests — Critical Services **What:** Write tests for the most critical business logic: + 1. `working-days.test.ts` — Orthodox Easter 2024-2030, addWorkingDays, backward deadlines 2. `deadline-service.test.ts` — Due date computation, tacit approval, chain resolution 3. `registry-service.test.ts` — Number generation, overdue calculation @@ -272,6 +291,7 @@ npm install -D vitest @testing-library/react @testing-library/jest-dom jsdom @vi ### 3.03 `[STANDARD]` Data Export/Import for All Modules **What:** Create a shared utility for backing up localStorage data: + 1. Per-module JSON export (download file) 2. Per-module JSON import (upload + merge) 3. Full backup: export ALL modules as single JSON @@ -284,6 +304,7 @@ npm install -D vitest @testing-library/react @testing-library/jest-dom jsdom @vi ### 3.04 `[LIGHT]` Update Stale Documentation **What:** Update docs to reflect current state: + - `docs/architecture/SYSTEM-ARCHITECTURE.md` — Change modules from "Planned" to "Implemented" - `docs/DATA-MODEL.md` — Add TrackedDeadline, Hot Desk schemas - `docs/REPO-STRUCTURE.md` — Add new files @@ -303,6 +324,7 @@ npm install -D vitest @testing-library/react @testing-library/jest-dom jsdom @vi ### 4.01 `[HEAVY]` AI Chat — Real API Integration **What:** Replace demo mode with actual AI provider calls: + - Create `/api/ai/chat` server-side route (API keys never exposed to browser) - Provider abstraction: Anthropic Claude, OpenAI GPT, Ollama (local) - Response streaming via ReadableStream @@ -310,6 +332,7 @@ npm install -D vitest @testing-library/react @testing-library/jest-dom jsdom @vi - Token usage display **Env vars:** + ``` ANTHROPIC_API_KEY=sk-ant-... OPENAI_API_KEY=sk-... @@ -325,6 +348,7 @@ AI_DEFAULT_MODEL=claude-sonnet-4-6-20261001 ### 4.02 `[STANDARD]` AI Chat — Domain-Specific System Prompts **What:** Architecture office-focused conversation modes: + - Romanian construction law assistant - Architectural visualization prompt crafter - Technical specification writer @@ -347,12 +371,14 @@ AI_DEFAULT_MODEL=claude-sonnet-4-6-20261001 ### 5.01 `[HEAVY]` Authentik OIDC Integration **What:** Replace stub user with real Authentik SSO. + - NextAuth.js / Auth.js route handler - OIDC token → user profile resolution - Cookie-based session - `useAuth()` returns real user **Server setup required:** + 1. Create OAuth2 app in Authentik (http://10.10.10.166:9100) 2. Set redirect URI: `http://10.10.10.166:3000/api/auth/callback/authentik` 3. Set env vars: `AUTHENTIK_URL`, `AUTHENTIK_CLIENT_ID`, `AUTHENTIK_CLIENT_SECRET`, `NEXTAUTH_SECRET` @@ -499,22 +525,23 @@ AI_DEFAULT_MODEL=claude-sonnet-4-6-20261001 ## Infrastructure Credentials Needed -| Service | What | When Needed | -|---|---|---| -| **US/SDT Logos** | SVG/PNG logo files | Phase 1 (task 1.01) | -| **US/SDT Addresses** | Office addresses for email signature | Phase 1 (task 1.02) | -| **Anthropic API Key** | `sk-ant-...` from console.anthropic.com | Phase 4 (task 4.01) | -| **OpenAI API Key** | `sk-...` from platform.openai.com | Phase 4 (task 4.01) | -| **Authentik Admin** | Login to create OAuth app at :9100 | Phase 5 (task 5.01) | -| **MinIO Credentials** | Access key + secret key for :9003 | Phase 6 (task 6.04) | -| **PostgreSQL** | New container + password | Phase 6 (task 6.01) | -| **Gitea Actions Runner** | Registration token from Gitea admin | Phase 9 (task 9.01) | +| Service | What | When Needed | +| ------------------------ | --------------------------------------- | ------------------- | +| **US/SDT Logos** | SVG/PNG logo files | Phase 1 (task 1.01) | +| **US/SDT Addresses** | Office addresses for email signature | Phase 1 (task 1.02) | +| **Anthropic API Key** | `sk-ant-...` from console.anthropic.com | Phase 4 (task 4.01) | +| **OpenAI API Key** | `sk-...` from platform.openai.com | Phase 4 (task 4.01) | +| **Authentik Admin** | Login to create OAuth app at :9100 | Phase 5 (task 5.01) | +| **MinIO Credentials** | Access key + secret key for :9003 | Phase 6 (task 6.04) | +| **PostgreSQL** | New container + password | Phase 6 (task 6.01) | +| **Gitea Actions Runner** | Registration token from Gitea admin | Phase 9 (task 9.01) | --- ## Quick Picker **15 min tasks** `[LIGHT]`: + - 1.01 — Check logo files - 1.07 — Password vault company + strength - 1.08 — IT inventory contact link @@ -524,6 +551,7 @@ AI_DEFAULT_MODEL=claude-sonnet-4-6-20261001 - 3.05 — Wire env var URLs **1 hour tasks** `[STANDARD]`: + - 1.03 — Prompt generator templates - 1.04 — Tag manager seeds + mandatory - 1.05 — Mini utilities new tools @@ -533,6 +561,7 @@ AI_DEFAULT_MODEL=claude-sonnet-4-6-20261001 - 3.01 + 3.02 — Tests setup + core tests **Full session tasks** `[HEAVY]`: + - 2.01 — Hot Desk module (new) - 4.01 — AI Chat API integration - 5.01 — Authentik SSO diff --git a/SESSION-LOG.md b/SESSION-LOG.md index 0b7b7d1..4310754 100644 --- a/SESSION-LOG.md +++ b/SESSION-LOG.md @@ -7,6 +7,7 @@ ## Session — 2026-02-18 (GitHub Copilot - Haiku 4.5) ### Completed + - **Task 1.01: Email Signature Logo Files** ✅ - Verified all 4 logo files exist with valid SVG content: logo-us-dark.svg, logo-us-light.svg, logo-sdt-dark.svg, logo-sdt-light.svg - No action needed — logos are already present and valid @@ -18,9 +19,11 @@ - Build passes zero errors ### Commits + - `1db61d8` feat(email-signature): add address toggles for Urban Switch and Studii de Teren ### Notes + - Full npm install and build verification completed - Ready to move to task 1.03 (Prompt Generator architecture templates) after user approval - Set up git with AI Assistant user for commits @@ -30,6 +33,7 @@ ## Session — 2026-02-18 (Claude Opus 4.6) ### Completed + - **Registratura Legal Deadline Tracking** — Full implementation: - 9 new files: working-days.ts (Romanian holidays + Orthodox Easter), deadline-catalog.ts (16 deadline types), deadline-service.ts, use-deadline-filters.ts, deadline-card.tsx, deadline-add-dialog.tsx, deadline-resolve-dialog.tsx, deadline-table.tsx, deadline-dashboard.tsx - 6 modified files: types.ts, use-registry.ts, registratura-module.tsx (tabbed), registry-entry-form.tsx (inline deadlines), registry-table.tsx (clock badge), index.ts @@ -39,21 +43,22 @@ - **SESSION-GUIDE.md** — Created with start/resume prompts, git workflow, file update rules ### Commits + - `bb01268` feat(registratura): add legal deadline tracking system (Termene Legale) - `d6a5852` docs: add ROADMAP.md with detailed future task plan - `b1df15b` docs: rewrite ROADMAP.md with complete xlsx gap analysis + multi-model recommendations - (this session) docs: add SESSION-GUIDE.md + SESSION-LOG.md ### Notes + - Build passes with zero errors - Dev server on localhost:3000 shows tabs correctly - Production at 10.10.10.166:3000 requires Portainer redeploy after push - The `app_modules_overview.xlsx` is in the repo root but not committed (it's a reference file) - No tasks from ROADMAP.md Phase 1+ have been started yet — next session should begin with task 1.01 - - ### Completed + - **Registratura Legal Deadline Tracking** — Full implementation: - 9 new files: working-days.ts (Romanian holidays + Orthodox Easter), deadline-catalog.ts (16 deadline types), deadline-service.ts, use-deadline-filters.ts, deadline-card.tsx, deadline-add-dialog.tsx, deadline-resolve-dialog.tsx, deadline-table.tsx, deadline-dashboard.tsx - 6 modified files: types.ts, use-registry.ts, registratura-module.tsx (tabbed), registry-entry-form.tsx (inline deadlines), registry-table.tsx (clock badge), index.ts @@ -63,12 +68,14 @@ - **SESSION-GUIDE.md** — Created with start/resume prompts, git workflow, file update rules ### Commits + - `bb01268` feat(registratura): add legal deadline tracking system (Termene Legale) - `d6a5852` docs: add ROADMAP.md with detailed future task plan - `b1df15b` docs: rewrite ROADMAP.md with complete xlsx gap analysis + multi-model recommendations - (this session) docs: add SESSION-GUIDE.md + SESSION-LOG.md ### Notes + - Build passes with zero errors - Dev server on localhost:3000 shows tabs correctly - Production at 10.10.10.166:3000 requires Portainer redeploy after push diff --git a/src/config/companies.ts b/src/config/companies.ts index 292116b..4342c36 100644 --- a/src/config/companies.ts +++ b/src/config/companies.ts @@ -1,4 +1,4 @@ -import type { CompanyId } from '@/core/auth/types'; +import type { CompanyId } from "@/core/auth/types"; export interface Company { id: CompanyId; @@ -10,54 +10,54 @@ export interface Company { city: string; logo?: { light: string; // logo for light backgrounds - dark: string; // logo for dark backgrounds + dark: string; // logo for dark backgrounds }; } export const COMPANIES: Record = { beletage: { - id: 'beletage', - name: 'Beletage SRL', - shortName: 'Beletage', - cui: '', - color: '#22B5AB', - address: 'str. Unirii, nr. 3, ap. 26', - city: 'Cluj-Napoca', + id: "beletage", + name: "Beletage SRL", + shortName: "Beletage", + cui: "", + color: "#22B5AB", + address: "str. Unirii, nr. 3, ap. 26", + city: "Cluj-Napoca", }, - 'urban-switch': { - id: 'urban-switch', - name: 'Urban Switch SRL', - shortName: 'Urban Switch', - cui: '', - color: '#6366f1', - address: '', - city: 'Cluj-Napoca', + "urban-switch": { + id: "urban-switch", + name: "Urban Switch SRL", + shortName: "Urban Switch", + cui: "", + color: "#6366f1", + address: "", + city: "Cluj-Napoca", logo: { - light: '/logos/logo-us-light.svg', - dark: '/logos/logo-us-dark.svg', + light: "/logos/logo-us-light.svg", + dark: "/logos/logo-us-light.svg", }, }, - 'studii-de-teren': { - id: 'studii-de-teren', - name: 'Studii de Teren SRL', - shortName: 'Studii de Teren', - cui: '', - color: '#f59e0b', - address: '', - city: 'Cluj-Napoca', + "studii-de-teren": { + id: "studii-de-teren", + name: "Studii de Teren SRL", + shortName: "Studii de Teren", + cui: "", + color: "#f59e0b", + address: "", + city: "Cluj-Napoca", logo: { - light: '/logos/logo-sdt-dark.svg', - dark: '/logos/logo-sdt-light.svg', + light: "/logos/logo-sdt-light.svg", + dark: "/logos/logo-sdt-light.svg", }, }, group: { - id: 'group', - name: 'Grup Companii', - shortName: 'Grup', - cui: '', - color: '#64748b', - address: '', - city: 'Cluj-Napoca', + id: "group", + name: "Grup Companii", + shortName: "Grup", + cui: "", + color: "#64748b", + address: "", + city: "Cluj-Napoca", }, }; diff --git a/src/modules/email-signature/components/signature-configurator.tsx b/src/modules/email-signature/components/signature-configurator.tsx index f6061b0..3c8bf40 100644 --- a/src/modules/email-signature/components/signature-configurator.tsx +++ b/src/modules/email-signature/components/signature-configurator.tsx @@ -1,18 +1,38 @@ -'use client'; +"use client"; -import type { CompanyId } from '@/core/auth/types'; -import type { SignatureConfig, SignatureColors, SignatureLayout, SignatureVariant } from '../types'; -import { COMPANY_BRANDING, BELETAGE_ADDRESSES, US_ADDRESSES, SDT_ADDRESSES } from '../services/company-branding'; -import { Input } from '@/shared/components/ui/input'; -import { Label } from '@/shared/components/ui/label'; -import { Switch } from '@/shared/components/ui/switch'; -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/shared/components/ui/select'; -import { Separator } from '@/shared/components/ui/separator'; -import { cn } from '@/shared/lib/utils'; +import type { CompanyId } from "@/core/auth/types"; +import type { + SignatureConfig, + SignatureColors, + SignatureLayout, + SignatureVariant, +} from "../types"; +import { + COMPANY_BRANDING, + BELETAGE_ADDRESSES, + US_ADDRESSES, + SDT_ADDRESSES, +} from "../services/company-branding"; +import type { AddressKey } from "../services/company-branding"; +import { Input } from "@/shared/components/ui/input"; +import { Label } from "@/shared/components/ui/label"; +import { Switch } from "@/shared/components/ui/switch"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/shared/components/ui/select"; +import { Separator } from "@/shared/components/ui/separator"; +import { cn } from "@/shared/lib/utils"; interface SignatureConfiguratorProps { config: SignatureConfig; - onUpdateField: (key: K, value: SignatureConfig[K]) => void; + onUpdateField: ( + key: K, + value: SignatureConfig[K], + ) => void; onUpdateColor: (key: keyof SignatureColors, value: string) => void; onUpdateLayout: (key: keyof SignatureLayout, value: number) => void; onSetVariant: (variant: SignatureVariant) => void; @@ -23,58 +43,71 @@ interface SignatureConfiguratorProps { /** Color palette per company */ const COMPANY_PALETTES: Record> = { beletage: { - verde: '#22B5AB', - griInchis: '#54504F', - griDeschis: '#A7A9AA', - negru: '#323232', + verde: "#22B5AB", + griInchis: "#54504F", + griDeschis: "#A7A9AA", + negru: "#323232", }, - 'urban-switch': { - indigo: '#6366f1', - violet: '#4F46E5', - griInchis: '#2D2D2D', - griDeschis: '#6B7280', - albastru: '#3B82F6', - negru: '#1F2937', + "urban-switch": { + albastru: "#345476", + griInchis: "#2D2D2D", + griDeschis: "#6B7280", + negru: "#1F2937", }, - 'studii-de-teren': { - amber: '#f59e0b', - portocaliu: '#D97706', - griInchis: '#2D2D2D', - griDeschis: '#6B7280', - maro: '#92400E', - negru: '#1F2937', + "studii-de-teren": { + teal: "#0182A1", + bleumarin: "#000D1A", + griInchis: "#2D2D2D", + griDeschis: "#6B7280", + negru: "#1F2937", }, group: { - gri: '#64748b', - griInchis: '#334155', - griDeschis: '#94a3b8', - negru: '#1e293b', + gri: "#64748b", + griInchis: "#334155", + griDeschis: "#94a3b8", + negru: "#1e293b", }, }; const COLOR_LABELS: Record = { - prefix: 'Titulatură', - name: 'Nume', - title: 'Funcție', - address: 'Adresă', - phone: 'Telefon', - website: 'Website', - motto: 'Motto', + prefix: "Titulatură", + name: "Nume", + title: "Funcție", + address: "Adresă", + phone: "Telefon", + website: "Website", + motto: "Motto", }; -const LAYOUT_CONTROLS: { key: keyof SignatureLayout; label: string; min: number; max: number }[] = [ - { key: 'greenLineWidth', label: 'Lungime linie accent', min: 50, max: 300 }, - { key: 'sectionSpacing', label: 'Spațiere secțiuni', min: 0, max: 30 }, - { key: 'logoSpacing', label: 'Spațiere logo', min: 0, max: 30 }, - { key: 'titleSpacing', label: 'Spațiere funcție', min: 0, max: 20 }, - { key: 'gutterWidth', label: 'Aliniere contact', min: 0, max: 150 }, - { key: 'iconTextSpacing', label: 'Spațiu icon-text', min: -10, max: 30 }, - { key: 'iconVerticalOffset', label: 'Aliniere verticală iconițe', min: -10, max: 10 }, - { key: 'mottoSpacing', label: 'Spațiere motto', min: 0, max: 20 }, +const LAYOUT_CONTROLS: { + key: keyof SignatureLayout; + label: string; + min: number; + max: number; +}[] = [ + { key: "greenLineWidth", label: "Lungime linie accent", min: 50, max: 300 }, + { key: "sectionSpacing", label: "Spațiere secțiuni", min: 0, max: 30 }, + { key: "logoSpacing", label: "Spațiere logo", min: 0, max: 30 }, + { key: "titleSpacing", label: "Spațiere funcție", min: 0, max: 20 }, + { key: "gutterWidth", label: "Aliniere contact", min: 0, max: 150 }, + { key: "iconTextSpacing", label: "Spațiu icon-text", min: -10, max: 30 }, + { + key: "iconVerticalOffset", + label: "Aliniere verticală iconițe", + min: -10, + max: 10, + }, + { key: "mottoSpacing", label: "Spațiere motto", min: 0, max: 20 }, ]; export function SignatureConfigurator({ - config, onUpdateField, onUpdateColor, onUpdateLayout, onSetVariant, onSetCompany, onSetAddress, + config, + onUpdateField, + onUpdateColor, + onUpdateLayout, + onSetVariant, + onSetCompany, + onSetAddress, }: SignatureConfiguratorProps) { const palette = COMPANY_PALETTES[config.company]; @@ -83,71 +116,129 @@ export function SignatureConfigurator({ {/* Company selector */}
- onSetCompany(v as CompanyId)} + > {Object.values(COMPANY_BRANDING).map((b) => ( - {b.name} + + {b.name} + ))}
{/* Address selector (for Beletage) */} - {config.company === 'beletage' && onSetAddress && ( + {config.company === "beletage" && onSetAddress && (
)} {/* Address selector (for Urban Switch) */} - {config.company === 'urban-switch' && onSetAddress && ( + {config.company === "urban-switch" && onSetAddress && (
)} {/* Address selector (for Studii de Teren) */} - {config.company === 'studii-de-teren' && onSetAddress && ( + {config.company === "studii-de-teren" && onSetAddress && (
@@ -160,19 +251,40 @@ export function SignatureConfigurator({

Date personale

- onUpdateField('prefix', e.target.value)} className="mt-1" /> + onUpdateField("prefix", e.target.value)} + className="mt-1" + />
- onUpdateField('name', e.target.value)} className="mt-1" /> + onUpdateField("name", e.target.value)} + className="mt-1" + />
- onUpdateField('title', e.target.value)} className="mt-1" /> + onUpdateField("title", e.target.value)} + className="mt-1" + />
- onUpdateField('phone', e.target.value)} className="mt-1" /> + onUpdateField("phone", e.target.value)} + className="mt-1" + />
@@ -181,19 +293,32 @@ export function SignatureConfigurator({ {/* Variant */}

Variantă

- onSetVariant(v as SignatureVariant)} + > - Completă (logo + adresă + motto) + + Completă (logo + adresă + motto) + Simplă (fără logo/adresă) - Super-simplă (doar nume/telefon) + + Super-simplă (doar nume/telefon) +
- onUpdateField('useSvg', v)} id="svg-toggle" /> - + onUpdateField("useSvg", v)} + id="svg-toggle" + /> +
@@ -202,27 +327,31 @@ export function SignatureConfigurator({ {/* Colors — company-specific palette */}

Culori text

- {(Object.keys(COLOR_LABELS) as (keyof SignatureColors)[]).map((colorKey) => ( -
- {COLOR_LABELS[colorKey]} -
- {Object.values(palette).map((color) => ( -
-
- ))} + ), + )} @@ -234,14 +363,18 @@ export function SignatureConfigurator({
- {config.layout[key]}px + + {config.layout[key]}px +
onUpdateLayout(key, parseInt(e.target.value, 10))} + onChange={(e) => + onUpdateLayout(key, parseInt(e.target.value, 10)) + } className="mt-1 w-full accent-primary" />
diff --git a/src/modules/email-signature/services/company-branding.ts b/src/modules/email-signature/services/company-branding.ts index ff4b189..4787e25 100644 --- a/src/modules/email-signature/services/company-branding.ts +++ b/src/modules/email-signature/services/company-branding.ts @@ -1,132 +1,150 @@ -import type { CompanyId } from '@/core/auth/types'; -import type { CompanyBranding, SignatureColors } from '../types'; +import type { CompanyId } from "@/core/auth/types"; +import type { CompanyBranding, SignatureColors } from "../types"; const BELETAGE_COLORS: SignatureColors = { - prefix: '#54504F', - name: '#54504F', - title: '#A7A9AA', - address: '#A7A9AA', - phone: '#54504F', - website: '#54504F', - motto: '#22B5AB', + prefix: "#54504F", + name: "#54504F", + title: "#A7A9AA", + address: "#A7A9AA", + phone: "#54504F", + website: "#54504F", + motto: "#22B5AB", }; const URBAN_SWITCH_COLORS: SignatureColors = { - prefix: '#2D2D2D', - name: '#2D2D2D', - title: '#6B7280', - address: '#6B7280', - phone: '#2D2D2D', - website: '#4F46E5', - motto: '#6366f1', + prefix: "#345476", + name: "#345476", + title: "#6B7280", + address: "#6B7280", + phone: "#345476", + website: "#345476", + motto: "#345476", }; const STUDII_COLORS: SignatureColors = { - prefix: '#2D2D2D', - name: '#2D2D2D', - title: '#6B7280', - address: '#6B7280', - phone: '#2D2D2D', - website: '#D97706', - motto: '#f59e0b', + prefix: "#000D1A", + name: "#000D1A", + title: "#6B7280", + address: "#6B7280", + phone: "#000D1A", + website: "#0182A1", + motto: "#0182A1", }; -const ADDR_UNIRII = ['str. Unirii, nr. 3, ap. 26', 'Cluj-Napoca, Cluj 400417', 'România'] as const; -const ADDR_CHRISTESCU = ['str. G-ral Eremia Grigorescu, nr. 21', 'Cluj-Napoca, Cluj 400304', 'România'] as const; +const ADDR_CHRISTESCU = [ + "str. G-ral Constantin Christescu, nr. 12", + "Cluj-Napoca, Cluj 400416", + "România", +] as const; +const ADDR_UNIRII = [ + "str. Unirii, nr. 3, sc. 3 ap. 26", + "Cluj-Napoca, Cluj 400432", + "România", +] as const; +const ADDR_ALBAC = [ + "Str. Albac, nr. 2, ap. 1", + "Cluj-Napoca, Cluj 400459", + "România", +] as const; + +/** Address option keys shared across all companies */ +export type AddressKey = "christescu" | "unirii" | "albac"; /** Available address options for Beletage (toggle between offices) */ -export const BELETAGE_ADDRESSES: { unirii: string[]; christescu: string[] } = { - unirii: [...ADDR_UNIRII], +export const BELETAGE_ADDRESSES: Record = { christescu: [...ADDR_CHRISTESCU], + unirii: [...ADDR_UNIRII], + albac: [...ADDR_ALBAC], }; /** Available address options for Urban Switch */ -export const US_ADDRESSES: { unirii: string[] } = { - unirii: ['str. Unirii, nr. 3, ap. 26', 'Cluj-Napoca, Cluj 400417', 'România'], +export const US_ADDRESSES: Record = { + christescu: [...ADDR_CHRISTESCU], + unirii: [...ADDR_UNIRII], + albac: [...ADDR_ALBAC], }; /** Available address options for Studii de Teren */ -export const SDT_ADDRESSES: { unirii: string[] } = { - unirii: ['str. Unirii, nr. 3, ap. 26', 'Cluj-Napoca, Cluj 400417', 'românia'], +export const SDT_ADDRESSES: Record = { + christescu: [...ADDR_CHRISTESCU], + unirii: [...ADDR_UNIRII], + albac: [...ADDR_ALBAC], }; export const COMPANY_BRANDING: Record = { beletage: { - id: 'beletage', - name: 'Beletage SRL', - accent: '#22B5AB', + id: "beletage", + name: "Beletage SRL", + accent: "#22B5AB", logo: { - png: 'https://beletage.ro/img/Semnatura-Logo.png', - svg: 'https://beletage.ro/img/Logo-Beletage.svg', + png: "https://beletage.ro/img/Semnatura-Logo.png", + svg: "https://beletage.ro/img/Logo-Beletage.svg", }, slashGrey: { - png: 'https://beletage.ro/img/Grey-slash.png', - svg: 'https://beletage.ro/img/Grey-slash.svg', + png: "https://beletage.ro/img/Grey-slash.png", + svg: "https://beletage.ro/img/Grey-slash.svg", }, slashAccent: { - png: 'https://beletage.ro/img/Green-slash.png', - svg: 'https://beletage.ro/img/Green-slash.svg', + png: "https://beletage.ro/img/Green-slash.png", + svg: "https://beletage.ro/img/Green-slash.svg", }, - address: [...ADDR_UNIRII], - website: 'www.beletage.ro', - motto: 'we make complex simple', + logoDimensions: { width: 162, height: 24 }, + address: [...ADDR_CHRISTESCU], + website: "www.beletage.ro", + motto: "we make complex simple", defaultColors: BELETAGE_COLORS, }, - 'urban-switch': { - id: 'urban-switch', - name: 'Urban Switch SRL', - accent: '#6366f1', + "urban-switch": { + id: "urban-switch", + name: "Urban Switch SRL", + accent: "#345476", logo: { - png: '/logos/logo-us-dark.svg', - svg: '/logos/logo-us-dark.svg', + png: "/logos/logo-us-light.svg", + svg: "/logos/logo-us-light.svg", }, slashGrey: { - png: 'https://beletage.ro/img/Grey-slash.png', - svg: 'https://beletage.ro/img/Grey-slash.svg', + png: "https://beletage.ro/img/Grey-slash.png", + svg: "https://beletage.ro/img/Grey-slash.svg", }, - slashAccent: { - png: '/logos/logo-us-light.svg', - svg: '/logos/logo-us-light.svg', - }, - address: ['str. Unirii, nr. 3, ap. 26', 'Cluj-Napoca, Cluj 400417', 'România'], - website: 'www.urbanswitch.ro', - motto: 'shaping urban futures', + slashAccent: { png: "", svg: "" }, + logoDimensions: { width: 140, height: 24 }, + address: [...ADDR_CHRISTESCU], + website: "www.urbanswitch.ro", + motto: "shaping urban futures", defaultColors: URBAN_SWITCH_COLORS, }, - 'studii-de-teren': { - id: 'studii-de-teren', - name: 'Studii de Teren SRL', - accent: '#f59e0b', + "studii-de-teren": { + id: "studii-de-teren", + name: "Studii de Teren SRL", + accent: "#0182A1", logo: { - png: '/logos/logo-sdt-dark.svg', - svg: '/logos/logo-sdt-dark.svg', + png: "/logos/logo-sdt-light.svg", + svg: "/logos/logo-sdt-light.svg", }, slashGrey: { - png: 'https://beletage.ro/img/Grey-slash.png', - svg: 'https://beletage.ro/img/Grey-slash.svg', + png: "https://beletage.ro/img/Grey-slash.png", + svg: "https://beletage.ro/img/Grey-slash.svg", }, - slashAccent: { - png: '/logos/logo-sdt-light.svg', - svg: '/logos/logo-sdt-light.svg', - }, - address: ['str. Unirii, nr. 3, ap. 26', 'Cluj-Napoca, Cluj 400417', 'România'], - website: 'www.studiideteren.ro', - motto: 'ground truth, measured right', + slashAccent: { png: "", svg: "" }, + logoDimensions: { width: 71, height: 24 }, + address: [...ADDR_CHRISTESCU], + website: "www.studiideteren.ro", + motto: "ground truth, measured right", defaultColors: STUDII_COLORS, }, group: { - id: 'group', - name: 'Grup Companii', - accent: '#64748b', - logo: { png: '', svg: '' }, + id: "group", + name: "Grup Companii", + accent: "#64748b", + logo: { png: "", svg: "" }, slashGrey: { - png: 'https://beletage.ro/img/Grey-slash.png', - svg: 'https://beletage.ro/img/Grey-slash.svg', + png: "https://beletage.ro/img/Grey-slash.png", + svg: "https://beletage.ro/img/Grey-slash.svg", }, - slashAccent: { png: '', svg: '' }, - address: ['Cluj-Napoca, Cluj', 'România'], - website: '', - motto: '', + slashAccent: { png: "", svg: "" }, + address: ["Cluj-Napoca, Cluj", "România"], + website: "", + motto: "", defaultColors: BELETAGE_COLORS, }, }; diff --git a/src/modules/email-signature/services/signature-builder.ts b/src/modules/email-signature/services/signature-builder.ts index 5c70c6e..7a95caf 100644 --- a/src/modules/email-signature/services/signature-builder.ts +++ b/src/modules/email-signature/services/signature-builder.ts @@ -1,9 +1,9 @@ -import type { SignatureConfig, CompanyBranding } from '../types'; -import { getBranding } from './company-branding'; +import type { SignatureConfig, CompanyBranding } from "../types"; +import { getBranding } from "./company-branding"; export function formatPhone(raw: string): { display: string; link: string } { - const clean = raw.replace(/\s/g, ''); - if (clean.length === 10 && clean.startsWith('07')) { + const clean = raw.replace(/\s/g, ""); + if (clean.length === 10 && clean.startsWith("07")) { return { display: `+40 ${clean.substring(1, 4)} ${clean.substring(4, 7)} ${clean.substring(7, 10)}`, link: `tel:+40${clean.substring(1)}`, @@ -17,30 +17,47 @@ export function generateSignatureHtml(config: SignatureConfig): string { const address = config.addressOverride ?? branding.address; const { display: phone, link: phoneLink } = formatPhone(config.phone); const images = config.useSvg - ? { logo: branding.logo.svg, greySlash: branding.slashGrey.svg, accentSlash: branding.slashAccent.svg } - : { logo: branding.logo.png, greySlash: branding.slashGrey.png, accentSlash: branding.slashAccent.png }; + ? { + logo: branding.logo.svg, + greySlash: branding.slashGrey.svg, + accentSlash: branding.slashAccent.svg, + } + : { + logo: branding.logo.png, + greySlash: branding.slashGrey.png, + accentSlash: branding.slashAccent.png, + }; const { - greenLineWidth, gutterWidth, iconTextSpacing, iconVerticalOffset, - mottoSpacing, sectionSpacing, titleSpacing, logoSpacing, + greenLineWidth, + gutterWidth, + iconTextSpacing, + iconVerticalOffset, + mottoSpacing, + sectionSpacing, + titleSpacing, + logoSpacing, } = config.layout; const colors = config.colors; - const isReply = config.variant === 'reply' || config.variant === 'minimal'; - const isMinimal = config.variant === 'minimal'; + const isReply = config.variant === "reply" || config.variant === "minimal"; + const isMinimal = config.variant === "minimal"; - const hide = 'mso-hide:all;display:none!important;max-height:0;overflow:hidden;font-size:0;line-height:0;'; - const hideTitle = isReply ? hide : ''; - const hideLogo = isReply ? hide : ''; - const hideBottom = isMinimal ? hide : ''; - const hidePhoneIcon = isMinimal ? hide : ''; + const logoDim = branding.logoDimensions ?? { width: 162, height: 24 }; + + const hide = + "mso-hide:all;display:none!important;max-height:0;overflow:hidden;font-size:0;line-height:0;"; + const hideTitle = isReply ? hide : ""; + const hideLogo = isReply ? hide : ""; + const hideBottom = isMinimal ? hide : ""; + const hidePhoneIcon = isMinimal ? hide : ""; const spacerWidth = Math.max(0, iconTextSpacing); const textPaddingLeft = Math.max(0, -iconTextSpacing); const prefixHtml = config.prefix ? `${esc(config.prefix)} ` - : ''; + : ""; return ` @@ -57,28 +74,32 @@ export function generateSignatureHtml(config: SignatureConfig): string { - - ${branding.website ? `` : ''} + ${branding.website ? `` : ""} - ${branding.motto ? `` : ''} + ${branding.motto ? `` : ""}
- ${images.logo ? ` - ${esc(branding.name)} - ` : ''} + ${ + images.logo + ? ` + ${esc(branding.name)} + ` + : "" + }
+
- ${images.greySlash ? `` : ''} + ${images.greySlash ? `` : ""} - ${address.join('
')}
+ ${address.join("
")}
- ${images.accentSlash ? `` : ''} + ${images.accentSlash ? `` : ""} @@ -89,7 +110,7 @@ export function generateSignatureHtml(config: SignatureConfig): string {
${branding.website}
${branding.website}
@@ -100,22 +121,22 @@ export function generateSignatureHtml(config: SignatureConfig): string {
${esc(branding.motto)}
${esc(branding.motto)}
`; } function esc(text: string): string { return text - .replace(/&/g, '&') - .replace(//g, '>') - .replace(/"/g, '"'); + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """); } export function downloadSignatureHtml(html: string, filename: string): void { - const blob = new Blob([html], { type: 'text/html' }); - const a = document.createElement('a'); + const blob = new Blob([html], { type: "text/html" }); + const a = document.createElement("a"); a.href = URL.createObjectURL(blob); a.download = filename; document.body.appendChild(a); diff --git a/src/modules/email-signature/types.ts b/src/modules/email-signature/types.ts index 95ade11..cd220a9 100644 --- a/src/modules/email-signature/types.ts +++ b/src/modules/email-signature/types.ts @@ -1,6 +1,6 @@ -import type { CompanyId } from '@/core/auth/types'; +import type { CompanyId } from "@/core/auth/types"; -export type SignatureVariant = 'full' | 'reply' | 'minimal'; +export type SignatureVariant = "full" | "reply" | "minimal"; export interface SignatureColors { prefix: string; @@ -30,6 +30,8 @@ export interface CompanyBranding { logo: { png: string; svg: string }; slashGrey: { png: string; svg: string }; slashAccent: { png: string; svg: string }; + /** Logo dimensions (width × height) for the signature HTML */ + logoDimensions?: { width: number; height: number }; address: string[]; website: string; motto: string; diff --git a/src/shared/components/layout/sidebar.tsx b/src/shared/components/layout/sidebar.tsx index dd4c0c6..5ae18bc 100644 --- a/src/shared/components/layout/sidebar.tsx +++ b/src/shared/components/layout/sidebar.tsx @@ -1,26 +1,50 @@ -'use client'; +"use client"; -import Image from 'next/image'; -import Link from 'next/link'; -import { usePathname } from 'next/navigation'; -import { useTheme } from 'next-themes'; -import { useMemo } from 'react'; -import * as Icons from 'lucide-react'; -import { buildNavigation } from '@/config/navigation'; -import { COMPANIES } from '@/config/companies'; -import { useFeatureFlag } from '@/core/feature-flags'; -import { cn } from '@/shared/lib/utils'; -import { ScrollArea } from '@/shared/components/ui/scroll-area'; -import { Separator } from '@/shared/components/ui/separator'; +import Image from "next/image"; +import Link from "next/link"; +import { usePathname } from "next/navigation"; +import { useMemo } from "react"; +import * as Icons from "lucide-react"; +import { buildNavigation } from "@/config/navigation"; +import { COMPANIES } from "@/config/companies"; +import { useFeatureFlag } from "@/core/feature-flags"; +import { cn } from "@/shared/lib/utils"; +import { ScrollArea } from "@/shared/components/ui/scroll-area"; +import { Separator } from "@/shared/components/ui/separator"; -function DynamicIcon({ name, className }: { name: string; className?: string }) { - const pascalName = name.replace(/(^|-)([a-z])/g, (_, _p, c: string) => c.toUpperCase()); - const IconComponent = (Icons as unknown as Record>)[pascalName]; +function DynamicIcon({ + name, + className, +}: { + name: string; + className?: string; +}) { + const pascalName = name.replace(/(^|-)([a-z])/g, (_, _p, c: string) => + c.toUpperCase(), + ); + const IconComponent = ( + Icons as unknown as Record< + string, + React.ComponentType<{ className?: string }> + > + )[pascalName]; if (!IconComponent) return ; return ; } -function NavItem({ item, isActive }: { item: { id: string; label: string; icon: string; href: string; featureFlag: string }; isActive: boolean }) { +function NavItem({ + item, + isActive, +}: { + item: { + id: string; + label: string; + icon: string; + href: string; + featureFlag: string; + }; + isActive: boolean; +}) { const enabled = useFeatureFlag(item.featureFlag); if (!enabled) return null; @@ -28,10 +52,10 @@ function NavItem({ item, isActive }: { item: { id: string; label: string; icon: @@ -41,11 +65,9 @@ function NavItem({ item, isActive }: { item: { id: string; label: string; icon: } function SidebarLogo() { - const { resolvedTheme } = useTheme(); - const sdt = COMPANIES['studii-de-teren']; - const logoSrc = sdt.logo - ? (resolvedTheme === 'dark' ? sdt.logo.dark : sdt.logo.light) - : null; + const sdt = COMPANIES["studii-de-teren"]; + + const logoSrc = sdt.logo?.light ?? null; if (!logoSrc) { return ; @@ -58,6 +80,7 @@ function SidebarLogo() { width={28} height={28} className="h-7 w-7 shrink-0" + suppressHydrationWarning /> ); } @@ -77,10 +100,10 @@ export function Sidebar() {