Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8f65efd5d1 | |||
| eab465b8c3 | |||
| 0c4b91707f |
@@ -1,463 +1,197 @@
|
||||
# ArchiTools — Project Context for AI Assistants
|
||||
|
||||
> This file provides all context needed for Claude Code, Sonnet, or any AI model to work on this project from scratch.
|
||||
|
||||
---
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
npm install
|
||||
npm run dev # http://localhost:3000
|
||||
npx next build # verify zero errors before pushing
|
||||
git push origin main # auto-deploys via Portainer webhook
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Project Overview
|
||||
|
||||
**ArchiTools** is a modular internal web dashboard for an architecture/engineering office group of 3 companies:
|
||||
|
||||
- **Beletage** (architecture)
|
||||
- **Urban Switch** (urbanism)
|
||||
- **Studii de Teren** (geotechnics)
|
||||
|
||||
It runs on two on-premise servers, containerized with Docker, managed via Portainer CE.
|
||||
|
||||
### Stack
|
||||
|
||||
| Layer | Technology |
|
||||
| ------------ | ---------------------------------------------------------------------------- |
|
||||
| Framework | Next.js 16.x, App Router, TypeScript (strict) |
|
||||
| Styling | Tailwind CSS v4, shadcn/ui |
|
||||
| Database | PostgreSQL (10.10.10.166:5432) via Prisma v6 ORM |
|
||||
| Storage | `DatabaseStorageAdapter` (PostgreSQL) — localStorage fallback available |
|
||||
| File Storage | MinIO (10.10.10.166:9002 API / 9003 UI) — client configured, adapter pending |
|
||||
| Auth | NextAuth v4 + Authentik OIDC (auth.beletage.ro) |
|
||||
| Proxy | Traefik v3 on `10.10.10.199` (proxy server), SSL via Let's Encrypt |
|
||||
| Deploy | Docker multi-stage, Portainer CE on `10.10.10.166` (satra) |
|
||||
| Repo | Gitea at `https://git.beletage.ro/gitadmin/ArchiTools` |
|
||||
| Language | Code in **English**, UI in **Romanian** |
|
||||
|
||||
### Architecture Principles
|
||||
|
||||
- **Module platform, not monolith** — each module isolated with own types/services/hooks/components
|
||||
- **Feature flags** gate module loading (disabled = zero bundle cost)
|
||||
- **Storage abstraction**: `StorageService` interface with adapters (database default via Prisma, localStorage fallback)
|
||||
- **Cross-module tagging system** as shared service
|
||||
- **Auth via Authentik SSO** — NextAuth v4 + OIDC, group→role/company mapping
|
||||
- **All entities** include `visibility` / `createdBy` fields from day one
|
||||
- **Company logos** — theme-aware (light/dark variants), dual-rendered for SSR safety
|
||||
|
||||
---
|
||||
|
||||
## Repository Structure
|
||||
|
||||
```
|
||||
src/
|
||||
├── app/ # Routing only (thin wrappers)
|
||||
│ ├── (modules)/ # Module route pages
|
||||
│ └── layout.tsx # App shell
|
||||
├── core/ # Platform services
|
||||
│ ├── module-registry/ # Module registration + types
|
||||
│ ├── feature-flags/ # Flag evaluation + env override
|
||||
│ ├── storage/ # StorageService + adapters
|
||||
│ │ └── adapters/ # localStorage adapter (+ future DB/MinIO)
|
||||
│ ├── tagging/ # Cross-module tag service
|
||||
│ ├── i18n/ # Romanian translations
|
||||
│ ├── theme/ # Light/dark theme
|
||||
│ └── auth/ # Auth types + stub (future Authentik)
|
||||
├── modules/ # Module business logic
|
||||
│ ├── <module-name>/
|
||||
│ │ ├── components/ # Module UI components
|
||||
│ │ ├── hooks/ # Module-specific hooks
|
||||
│ │ ├── services/ # Module business logic
|
||||
│ │ ├── types.ts # Module types
|
||||
│ │ ├── config.ts # Module metadata
|
||||
│ │ └── index.ts # Public exports
|
||||
│ └── ...
|
||||
├── shared/ # Shared UI
|
||||
│ ├── components/
|
||||
│ │ ├── ui/ # shadcn/ui primitives
|
||||
│ │ ├── layout/ # Sidebar, Header
|
||||
│ │ └── common/ # Reusable app components
|
||||
│ ├── hooks/ # Shared hooks
|
||||
│ └── lib/ # Utils (cn, etc.)
|
||||
├── config/ # Global config
|
||||
│ ├── modules.ts # Module registry entries
|
||||
│ ├── flags.ts # Default feature flags
|
||||
│ ├── navigation.ts # Sidebar nav structure
|
||||
│ └── companies.ts # Company definitions
|
||||
docs/ # 16 internal technical docs
|
||||
legacy/ # Original HTML tools for reference
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Implemented Modules (16 total — 14 original + 2 new)
|
||||
|
||||
| # | Module | Route | Version | Key Features |
|
||||
| --- | ---------------------- | --------------------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| 1 | **Dashboard** | `/` | 0.1.0 | KPI cards (6), activity feed (last 20), module grid, external tools |
|
||||
| 2 | **Email Signature** | `/email-signature` | 0.1.0 | Multi-company branding, address toggle (BTG/US/SDT), live preview, zoom/copy/download |
|
||||
| 3 | **Word XML Generator** | `/word-xml` | 0.1.0 | Category-based XML gen, simple/advanced mode, ZIP export |
|
||||
| 4 | **Registratura** | `/registratura` | 0.5.0 | CRUD registry, dynamic doc types, bidirectional Address Book, threads, backdating, **legal deadline tracking** (6 categories, 18 types), recipient registration, document expiry, **NAS network path attachments** (A/O/P/T drives, copy-to-clipboard), **detail sheet side panel**, **configurable column visibility**, **QuickLook attachment preview** (images: zoom/pan, PDFs: native viewer, multi-file navigation), **email notifications** (Brevo SMTP daily digest, per-user preferences), **compact registry numbers** (single-letter company badge + direction arrow + plain number) |
|
||||
| 5 | **Tag Manager** | `/tag-manager` | 0.2.0 | CRUD tags, category/scope/color, US/SDT seeds, mandatory categories, **ManicTime bidirectional sync** |
|
||||
| 6 | **IT Inventory** | `/it-inventory` | 0.2.0 | Dynamic equipment types, rented status (purple pulse), **42U rack visualization**, type/status/company filters |
|
||||
| 7 | **Address Book** | `/address-book` | 0.2.0 | CRUD contacts (person OR institution), card grid, vCard export, Registratura reverse lookup, **dynamic types (creatable)**, **name OR company required** (flexible validation), **ContactPerson with department field**, **quick contact from Registratura** (persons + institutions) |
|
||||
| 8 | **Password Vault** | `/password-vault` | 0.4.0 | CRUD credentials, 9 categorii cu iconițe, **WiFi QR code real**, context-aware form, strength meter, company scope, **AES-256-GCM encryption**, **utilizatori multipli per intrare** (VaultUser[]: username/password/email/notes, colapsibil în form, badge în list) |
|
||||
| 9 | **Mini Utilities** | `/mini-utilities` | 0.4.0 | Text case, char counter, percentage, **TVA calculator (cotă configurabilă: 5/9/19/21% + custom)**, area converter, U→R, num→text, artifact cleaner, MDLPA, **PDF compression** (qpdf local lossless + iLovePDF API cloud lossy, streaming upload for large files), PDF unlock, **DWG→DXF**, OCR, color palette, **paste on all drop zones**, **thermal drag-drop reorder**, **Calculator scară desen** (real cm↔desen mm, 7 preseturi 1:20..1:5000 + custom) |
|
||||
| 10 | **Prompt Generator** | `/prompt-generator` | 0.2.0 | Template-driven prompt builder, **18 templates** (14 text + 4 image), search bar, target type filter |
|
||||
| 11 | **Digital Signatures** | `/digital-signatures` | 0.1.0 | CRUD assets, drag-and-drop file upload, tag chips |
|
||||
| 12 | **Word Templates** | `/word-templates` | 0.1.0 | Template library, 8 categories, version tracking, .docx placeholder auto-detection |
|
||||
| 13 | **AI Chat** | `/ai-chat` | 0.2.0 | Multi-provider (OpenAI/Claude/Ollama/demo), **project linking via Tag Manager**, provider status badge |
|
||||
| 14 | **Hot Desk** | `/hot-desk` | 0.1.1 | 4 desks, week-ahead calendar, room layout (window+door proportioned), reserve/cancel |
|
||||
| 15 | **ParcelSync** | `/parcel-sync` | 0.6.0 | eTerra ANCPI integration, **PostGIS database**, background sync, 23-layer catalog, enrichment pipeline, owner search, **per-UAT analytics dashboard**, **health check + maintenance detection**, **ANCPI ePay CF extract ordering** (batch orders, MinIO PDF storage, dedup protection, credit tracking), **static WORKSPACE_TO_COUNTY mapping**, **GisUat geometry select optimization**, **feature count cache (5-min TTL)** |
|
||||
| 16 | **Visual Copilot** | `/visual-copilot` | 0.1.0 | AI-powered image analysis — **developed in separate repo** (`https://git.beletage.ro/gitadmin/vim`), placeholder in ArchiTools, will be merged as module later |
|
||||
|
||||
### Registratura — Legal Deadline Tracking (Termene Legale)
|
||||
|
||||
The Registratura module includes a full legal deadline tracking engine for Romanian construction permitting:
|
||||
|
||||
- **18 deadline types** across 6 categories (Certificat, Avize, Completari, Urbanism, Autorizare, Litigii)
|
||||
- **Working days vs calendar days** with Romanian public holiday support (including Orthodox Easter via Meeus algorithm)
|
||||
- **Chain deadlines** (resolving one prompts adding the next — e.g., CU analiza → emitere, PUZ/PUD analiza → post-CTATU → emitere)
|
||||
- **Tacit approval** (auto-detected when overdue + applicable type)
|
||||
- **Tabbed UI**: "Registru" tab (existing registry) + "Termene legale" tab (deadline dashboard)
|
||||
- **Email notifications**: daily digest via Brevo SMTP, per-user opt-in/opt-out preferences, N8N cron trigger
|
||||
|
||||
Key files:
|
||||
|
||||
- `services/working-days.ts` — Romanian holidays, `addWorkingDays()`, `isWorkingDay()`
|
||||
- `services/deadline-catalog.ts` — 18 `DeadlineTypeDef` entries across 6 categories
|
||||
- `services/deadline-service.ts` — `createTrackedDeadline()`, `resolveDeadline()`, `aggregateDeadlines()`
|
||||
- `components/attachment-preview.tsx` — QuickLook-style fullscreen preview (images: zoom/pan, PDFs: blob URL iframe, multi-file nav)
|
||||
- `components/deadline-dashboard.tsx` — Stats + filters + table
|
||||
- `components/deadline-add-dialog.tsx` — 3-step wizard (category → type → date preview)
|
||||
- `components/notification-preferences.tsx` — Bell button + dialog with per-type toggles
|
||||
- `components/registry-table.tsx` — `CompactNumber` component: single-letter company badge (B/U/S/G), direction arrow (↓ intrat / ↑ iesit), plain number
|
||||
|
||||
### Address Book — Flexible Contact Model
|
||||
|
||||
The Address Book supports both persons and institutions:
|
||||
|
||||
- **Flexible validation**: either `name` OR `company` required (not both mandatory)
|
||||
- **Auto-type detection**: when only company is set via quick-create, type defaults to "institution"
|
||||
- **ContactPerson sub-entities**: each has `name`, `department`, `role`, `email`, `phone`
|
||||
- **Quick contact creation from Registratura**: inline dialog with name + company + phone + email
|
||||
- **Display logic**: if no name, company shows as primary; if both, shows "Name (Company)"
|
||||
- **Creatable types**: dropdown with defaults (client/supplier/institution/collaborator/internal) + user-created custom types
|
||||
|
||||
Key files:
|
||||
|
||||
- `modules/address-book/types.ts` — `AddressContact`, `ContactPerson` interfaces
|
||||
- `modules/address-book/components/address-book-module.tsx` — Full UI (cards, detail dialog, form)
|
||||
- `modules/address-book/hooks/use-contacts.ts` — Storage hook with search/filter
|
||||
- `modules/address-book/services/vcard-export.ts` — vCard 3.0 export
|
||||
- `modules/registratura/components/quick-contact-dialog.tsx` — Quick create from registry
|
||||
|
||||
### PDF Compression — Dual Mode (Local + Cloud)
|
||||
|
||||
Two compression routes, both with streaming upload support for large files (tested up to 287MB):
|
||||
|
||||
- **Local (qpdf)**: lossless structural optimization — stream compression, object dedup, linearization. Safe, no font corruption. Typical reduction: 3-15%.
|
||||
- **Cloud (iLovePDF API)**: lossy image re-compression via iLovePDF REST API. Levels: extreme/recommended/low. Typical reduction: 50-91%. Requires `ILOVEPDF_PUBLIC_KEY` env var.
|
||||
|
||||
**Architecture** (zero-memory for any file size):
|
||||
1. `parseMultipartUpload()` streams request body to disk (constant 64KB memory)
|
||||
2. Scans raw file for multipart boundaries using `findInFile()` with 64KB sliding window
|
||||
3. Stream-copies PDF bytes to separate file
|
||||
4. Route handler processes (qpdf exec or iLovePDF API) and streams response back
|
||||
|
||||
**Critical gotchas**:
|
||||
- Middleware body buffering: `api/compress-pdf` routes are **excluded from middleware matcher** (middleware buffers entire body at 10MB default)
|
||||
- Auth: route-level `requireAuth()` instead of middleware (in `auth-check.ts`)
|
||||
- Unicode filenames: `Content-Disposition` header uses `encodeURIComponent()` to avoid ByteString errors with Romanian chars (Ș, Ț, etc.)
|
||||
- Ghostscript `-sDEVICE=pdfwrite` destroys font encodings — **never use GS for compression**, only qpdf
|
||||
|
||||
Key files:
|
||||
|
||||
- `app/api/compress-pdf/parse-upload.ts` — Streaming multipart parser (zero memory)
|
||||
- `app/api/compress-pdf/extreme/route.ts` — qpdf local compression
|
||||
- `app/api/compress-pdf/cloud/route.ts` — iLovePDF API integration
|
||||
- `app/api/compress-pdf/auth-check.ts` — Shared auth for routes excluded from middleware
|
||||
|
||||
### Email Notifications (Brevo SMTP)
|
||||
|
||||
Platform-level notification service for daily email digests:
|
||||
|
||||
- **Brevo SMTP relay** via nodemailer (port 587, STARTTLS)
|
||||
- **N8N cron**: weekdays 8:00 → POST `/api/notifications/digest` with Bearer token
|
||||
- **Per-user preferences**: stored in KeyValueStore (`notifications` namespace), toggle global opt-out + 3 notification types
|
||||
- **Digest content**: urgent deadlines (<=5 days), overdue deadlines, expiring documents (CU/AC)
|
||||
- **HTML email**: inline-styled table layout, color-coded rows (red/yellow/blue), per-company grouping
|
||||
- **Sender**: "Alerte Termene" <noreply@beletage.ro>, test mode via `?test=true` query param
|
||||
|
||||
Key files:
|
||||
|
||||
- `src/core/notifications/types.ts` — `NotificationType`, `NotificationPreference`, `DigestSection`, `DigestItem`
|
||||
- `src/core/notifications/email-service.ts` — Nodemailer transport singleton (Brevo SMTP)
|
||||
- `src/core/notifications/notification-service.ts` — `runDigest()`, `buildCompanyDigest()`, `renderDigestHtml()`, preference CRUD
|
||||
- `src/app/api/notifications/digest/route.ts` — POST endpoint (N8N cron, Bearer auth)
|
||||
- `src/app/api/notifications/preferences/route.ts` — GET/PUT (user session auth)
|
||||
|
||||
### ParcelSync — eTerra ANCPI GIS Integration
|
||||
|
||||
The ParcelSync module connects to Romania's national eTerra/ANCPI cadastral system:
|
||||
|
||||
- **eTerra API client** (`eterra-client.ts`): form-post auth, JSESSIONID cookie jar, session caching (9min TTL), auto-relogin, paginated fetching with `maxRecordCount=1000` + fallback page sizes (500, 200)
|
||||
- **23-layer catalog** (`eterra-layers.ts`): TERENURI_ACTIVE, CLADIRI_ACTIVE, LIMITE_UAT, etc. organized in 6 categories
|
||||
- **PostGIS storage**: `GisFeature` model with geometry column, SIRUTA-based partitioning, `enrichment` JSONB field
|
||||
- **Background sync**: long-running jobs via server singleton, progress polling (2s), phase tracking (fetch → save → enrich)
|
||||
- **Enrichment pipeline** (`enrich-service.ts`): hits eTerra `/api/immovable/list` per parcel to extract NR_CAD, NR_CF, PROPRIETARI, SUPRAFATA, INTRAVILAN, CATEGORIE_FOLOSINTA, HAS_BUILDING, etc.
|
||||
- **Owner search**: DB-first (ILIKE on enrichment JSON) with eTerra API fallback
|
||||
- **Per-UAT dashboard**: SQL aggregates (area stats, intravilan/extravilan, land use, top owners), CSS-only visualizations (donut ring, bar charts)
|
||||
- **Health check** (`eterra-health.ts`): pings `eterra.ancpi.ro` every 3min, detects maintenance by keywords in HTML response, blocks login when down, UI shows amber "Mentenanță" state
|
||||
- **ANCPI ePay CF extract ordering**: batch orders via `epay-client.ts`, PDF storage to MinIO, dedup protection (queue + API level), credit tracking
|
||||
- **Static county mapping**: `WORKSPACE_TO_COUNTY` in `county-refresh.ts` — 42 verified entries, preferred over unreliable nomenclature API
|
||||
- **Performance**: GisUat queries use `select` to exclude geometry column; feature counts cached 5-min TTL
|
||||
- **Test UAT**: Feleacu (SIRUTA 57582, ~30k immovables, ~8k GIS features)
|
||||
|
||||
Key files:
|
||||
|
||||
- `services/eterra-client.ts` — API client (~1000 lines), session cache, pagination, retry
|
||||
- `services/eterra-layers.ts` — 23-layer catalog with categories
|
||||
- `services/sync-service.ts` — Layer sync engine with progress tracking
|
||||
- `services/enrich-service.ts` — Enrichment pipeline (FeatureEnrichment type)
|
||||
- `services/eterra-health.ts` — Health check singleton, maintenance detection
|
||||
- `services/session-store.ts` — Server-side session management
|
||||
- `services/epay-client.ts` — ePay HTTP client (login, cart, metadata, submit, poll, download)
|
||||
- `services/epay-queue.ts` — Batch queue with dedup protection
|
||||
- `services/epay-storage.ts` — MinIO storage helpers for CF extract PDFs
|
||||
- `services/epay-counties.ts` — County index mapping (eTerra county name → ePay alphabetical index 0-41)
|
||||
- `app/api/eterra/session/county-refresh.ts` — Static `WORKSPACE_TO_COUNTY` mapping, LIMITE_UAT geometry refresh
|
||||
- `components/parcel-sync-module.tsx` — Main UI (~4100 lines), 5 tabs (Export/Layers/Search/DB/Extrase CF)
|
||||
- `components/uat-dashboard.tsx` — Per-UAT analytics dashboard (CSS-only charts)
|
||||
- `components/epay-tab.tsx` — CF extract ordering tab
|
||||
- `components/epay-connect.tsx` — ePay connection widget
|
||||
|
||||
---
|
||||
|
||||
## Infrastructure
|
||||
|
||||
### Server: `satra` — 10.10.10.166 (Ubuntu, app server)
|
||||
|
||||
| Service | Port | Purpose |
|
||||
| ----------------------- | ---------------------- | ----------------------------------- |
|
||||
| **ArchiTools** | 3000 | This app (tools.beletage.ro) |
|
||||
| **Gitea** | 3002 | Git hosting (git.beletage.ro) |
|
||||
| **PostgreSQL** | 5432 | App database (Prisma ORM) |
|
||||
| **Portainer CE** | 9000 | Docker management + deploy |
|
||||
| **Uptime Kuma** | 3001 | Service monitoring |
|
||||
| **MinIO** | 9002 (API) / 9003 (UI) | Object storage |
|
||||
| **Authentik** | 9100 | SSO (auth.beletage.ro) — **active** |
|
||||
| **N8N** | 5678 | Workflow automation (daily digest) |
|
||||
| **Stirling PDF** | 8087 | PDF tools |
|
||||
| **IT-Tools** | 8085 | Developer utilities |
|
||||
| **FileBrowser** | 8086 | File management |
|
||||
| **Netdata** | 19999 | System monitoring |
|
||||
| **Dozzle** | 9999 | Docker log viewer |
|
||||
| **CrowdSec** | 8088 | Security |
|
||||
|
||||
### Server: `proxy` — 10.10.10.199 (Traefik reverse proxy)
|
||||
|
||||
| Config | Path / Value |
|
||||
| ----------------------- | ---------------------------------------- |
|
||||
| **Static config** | `/opt/traefik/traefik.yml` |
|
||||
| **Dynamic configs** | `/opt/traefik/dynamic/` (file provider, `watch: true`) |
|
||||
| **ArchiTools route** | `/opt/traefik/dynamic/tools.yml` |
|
||||
| **SSL** | Let's Encrypt ACME, HTTP challenge |
|
||||
| **Timeouts** | `readTimeout: 600s`, `writeTimeout: 600s`, `idleTimeout: 600s` on `websecure` entrypoint |
|
||||
| **Response forwarding** | `flushInterval: 100ms` (streaming support) |
|
||||
|
||||
**IMPORTANT**: Default Traefik v2.11+ has 60s `readTimeout` — breaks large file uploads. Must set explicitly in static config.
|
||||
|
||||
### Deployment Pipeline
|
||||
|
||||
```
|
||||
git push origin main
|
||||
→ Gitea webhook fires
|
||||
→ Portainer CE: Stacks → architools → "Pull and redeploy"
|
||||
→ Toggle "Re-pull image and redeploy" ON → click "Update"
|
||||
→ Portainer re-clones git repo + Docker multi-stage build (~2 min)
|
||||
→ Container starts on :3000
|
||||
→ Traefik routes tools.beletage.ro → http://10.10.10.166:3000
|
||||
```
|
||||
|
||||
**Portainer CE deploy**: NOT automatic. Must manually click "Pull and redeploy" in Portainer UI after each push. The stack is configured from git repo `http://10.10.10.166:3002/gitadmin/ArchiTools`.
|
||||
|
||||
### Docker
|
||||
|
||||
- `Dockerfile`: 3-stage build (deps → builder → runner), `node:22-alpine`, non-root user
|
||||
- Runner stage installs: `gdal gdal-tools ghostscript qpdf` (for PDF compression, GIS)
|
||||
- `Dockerfile` includes `npx prisma generate` before build step
|
||||
- `docker-compose.yml`: single service, port 3000, **all env vars hardcoded** (Portainer CE can't inject env vars)
|
||||
- `output: 'standalone'` in `next.config.ts` is **required**
|
||||
- `@prisma/client` must be in `dependencies` (not devDependencies) for runtime
|
||||
|
||||
---
|
||||
|
||||
## Development Rules
|
||||
|
||||
### TypeScript Strict Mode Gotchas
|
||||
|
||||
- `array.split()[0]` returns `string | undefined` — use `.slice(0, 10)` instead
|
||||
- `Record<string, T>[key]` returns `T | undefined` — always guard with null check
|
||||
- Spread of possibly-undefined objects: `{ ...obj[key], field }` — check existence first
|
||||
- lucide-react Icons: cast through `unknown` → `React.ComponentType<{ className?: string }>`
|
||||
- `arr[0]` is `T | undefined` even after `arr.length > 0` check — assign to const first: `const first = arr[0]; if (first) { ... }`
|
||||
- Prisma `$queryRaw` returns `unknown[]` — always cast with `as Array<{ field: type }>` and guard access
|
||||
- `?? ""` on an object field typed `{}` produces `{}` not `string` — use explicit `typeof x === 'string'` or `'number'` check
|
||||
|
||||
### Conventions
|
||||
|
||||
- **Code**: English
|
||||
- **UI text**: Romanian
|
||||
- **Components**: functional, `'use client'` directive where needed
|
||||
- **State**: localStorage via `useStorage('module-name')` hook
|
||||
- **IDs**: `uuid v4`
|
||||
- **Dates**: ISO strings (`YYYY-MM-DD` for display, full ISO for timestamps)
|
||||
- **No emojis** in code or UI unless explicitly requested
|
||||
|
||||
### Storage Performance Rules
|
||||
|
||||
- **NEVER** use `storage.list()` followed by `storage.get()` in a loop — this is an N+1 query bug
|
||||
- `list()` fetches ALL items (keys+values) from DB but discards values, then each `get()` re-fetches individually
|
||||
- **ALWAYS** use `storage.exportAll()` (namespaced) or `storage.export(namespace)` (service-level) to batch-load
|
||||
- Filter items client-side after a single fetch: `for (const [key, value] of Object.entries(all)) { ... }`
|
||||
- After mutations (add/update), either do optimistic local state update or a single `refresh()` — never both
|
||||
- **NEVER store large binary data (base64 files) inside entity JSON** — this makes list loading transfer tens of MB
|
||||
- For modules with attachments: use `exportAll({ lightweight: true })` for listing, `storage.get()` for single-entry full load
|
||||
- The API `?lightweight=true` parameter strips `data`/`fileData` strings >1KB from JSON values server-side
|
||||
- Future: move file data to MinIO; only store metadata (name, size, type, url) in the entity JSON
|
||||
|
||||
### Module Development Pattern
|
||||
|
||||
Every module follows:
|
||||
|
||||
```
|
||||
src/modules/<name>/
|
||||
├── components/ # React components
|
||||
├── hooks/ # Custom hooks (use-<name>.ts)
|
||||
├── services/ # Business logic (pure functions)
|
||||
├── types.ts # TypeScript interfaces
|
||||
├── config.ts # ModuleConfig metadata
|
||||
└── index.ts # Public exports
|
||||
```
|
||||
|
||||
### Middleware & Large Upload Routes
|
||||
|
||||
- Next.js middleware buffers the **entire request body** even if it only reads cookies/headers
|
||||
- Default middleware body limit is 10MB — any upload route handling large files MUST be excluded
|
||||
- Excluded routes pattern in `src/middleware.ts` matcher: `api/auth|api/notifications/digest|api/compress-pdf`
|
||||
- Excluded routes handle auth via `requireAuth()` helper (`src/app/api/compress-pdf/auth-check.ts`)
|
||||
- To add a new large-upload route: (1) add to middleware matcher exclusion, (2) add `requireAuth()` call in route handler
|
||||
- `next.config.ts` has `experimental: { middlewareClientMaxBodySize: '500mb' }` but this is unreliable with `output: 'standalone'`
|
||||
|
||||
### eTerra / External API Rules
|
||||
|
||||
- **ArcGIS REST API** has `maxRecordCount=1000` — always paginate with `resultOffset`/`resultRecordCount`
|
||||
- **eTerra sessions expire after ~10min** — session cache TTL is 9min, auto-relogin on 401/redirect
|
||||
- **eTerra goes into maintenance regularly** — health check must detect and block login attempts
|
||||
- **Never hardcode timeouts too low** — eTerra 1000-feature geometry pages can take 60-90s; default is 120s
|
||||
- **CookieJar + axios-cookiejar-support** required for eTerra auth (JSESSIONID tracking)
|
||||
- **Page size fallbacks**: if 1000 fails, retry with 500, then 200
|
||||
- **WORKSPACE_TO_COUNTY is the authoritative county mapping** — static 42-entry map in `county-refresh.ts`, preferred over `fetchCounties()` which 404s intermittently
|
||||
- **GisUat.geometry is huge** — always use Prisma `select` to exclude it in list queries; forgetting this turns 50ms into 5+ seconds
|
||||
- **Feature counts are expensive** — cached in global with 5-min TTL in UATs route; returns stale data while refreshing
|
||||
|
||||
### ANCPI ePay Rules
|
||||
|
||||
- **ePay county IDs = eTerra WORKSPACE_IDs** (CLUJ=127, ALBA=10) — zero discovery calls needed
|
||||
- **ePay UAT IDs = SIRUTA codes** — use `GisUat.workspacePk` + `siruta` directly
|
||||
- **EpayJsonInterceptor uses form-urlencoded** (NOT JSON body) — `reqType=nomenclatorUAT&countyId=127`
|
||||
- **saveProductMetadataForBasketItem uses multipart/form-data** (form-data npm package)
|
||||
- **Document IDs are HTML-encoded** in ShowOrderDetails — `"idDocument":47301767` must be decoded before JSON parse
|
||||
- **ePay auth is OpenAM** — gets `AMAuthCookie`, then navigate to `http://` (not https) for JSESSIONID
|
||||
- **MinIO metadata must be ASCII** — strip diacritics from values before storing
|
||||
- Env vars: `ANCPI_USERNAME`, `ANCPI_PASSWORD`, `ANCPI_BASE_URL`, `ANCPI_LOGIN_URL`, `ANCPI_DEFAULT_SOLICITANT_ID`, `MINIO_BUCKET_ANCPI`
|
||||
|
||||
### Before Pushing
|
||||
|
||||
1. `npx next build` — must pass with zero errors
|
||||
2. Test the feature manually on `localhost:3000`
|
||||
3. Commit with descriptive message
|
||||
4. `git push origin main` — Portainer auto-deploys
|
||||
|
||||
---
|
||||
|
||||
## Company IDs
|
||||
|
||||
| ID | Name | Prefix |
|
||||
| ----------------- | --------------- | ------ |
|
||||
| `beletage` | Beletage | B |
|
||||
| `urban-switch` | Urban Switch | US |
|
||||
| `studii-de-teren` | Studii de Teren | SDT |
|
||||
| `group` | Grup | G |
|
||||
|
||||
---
|
||||
|
||||
## Current Integrations
|
||||
|
||||
| Feature | Status | Notes |
|
||||
| -------------------- | ---------------------- | ------------------------------------------------------------------------------------------ |
|
||||
| **Authentik SSO** | ✅ Active | NextAuth v4 + OIDC, group→role/company mapping |
|
||||
| **PostgreSQL** | ✅ Active | Prisma ORM, `KeyValueStore` model, `/api/storage` route |
|
||||
| **MinIO** | Client configured | 10.10.10.166:9002, bucket `tools`, adapter pending |
|
||||
| **AI Chat API** | ✅ Multi-provider | `/api/ai-chat` — OpenAI/Claude/Ollama/demo; needs API key env |
|
||||
| **Vault Encryption** | ✅ Active | AES-256-GCM server-side, `/api/vault`, ENCRYPTION_SECRET env |
|
||||
| **ManicTime Sync** | ✅ Implemented | `/api/manictime` — bidirectional Tags.txt sync, needs SMB mount |
|
||||
| **NAS Paths** | ✅ Active | `\\newamun` (10.10.10.10), drives A/O/P/T, hostname+IP fallback, `src/config/nas-paths.ts` |
|
||||
| **eTerra ANCPI** | ✅ Active | ParcelSync module, `eterra-client.ts`, health check + maintenance detection |
|
||||
| **ANCPI ePay** | ✅ Active | CF extract ordering, `epay-client.ts`, MinIO PDF storage, batch queue + dedup, `/api/ancpi/*` routes |
|
||||
| **PostGIS** | ✅ Active | `GisFeature` model, geometry storage, spatial queries, used by ParcelSync |
|
||||
| **Email Notifications** | ✅ Implemented | Brevo SMTP daily digest, `/api/notifications/digest` + `/preferences`, N8N cron trigger |
|
||||
| **N8N automations** | ✅ Active (digest cron) | Daily digest cron `0 8 * * 1-5`, Bearer token auth, future: backups, workflows |
|
||||
| **iLovePDF API** | ✅ Active | Cloud PDF compression, `ILOVEPDF_PUBLIC_KEY` env, free tier 250 files/month |
|
||||
| **qpdf** | ✅ Active | Local lossless PDF optimization, installed in Docker image (`apk add qpdf`) |
|
||||
|
||||
---
|
||||
|
||||
## Model Recommendations
|
||||
|
||||
| Task Type | Claude | OpenAI | Google | Notes |
|
||||
| ----------------------------- | -------------- | ------------- | ---------------- | ----------------------------------------------- |
|
||||
| **Bug fixes, config** | Haiku 4.5 | GPT-4o-mini | Gemini 2.5 Flash | Fast, cheap |
|
||||
| **Features, tests, UI** | **Sonnet 4.6** | GPT-5.2 | Gemini 3 Flash | Best value — Opus-class quality at Sonnet price |
|
||||
| **New modules, architecture** | Opus 4.6 | GPT-5.3-Codex | Gemini 3 Pro | Complex multi-file, business logic |
|
||||
|
||||
**Default: Sonnet 4.6** for most work. See `ROADMAP.md` for per-task recommendations.
|
||||
|
||||
### Session Handoff Tips
|
||||
|
||||
- Read this `CLAUDE.md` first — it has all context
|
||||
- Read `ROADMAP.md` for the complete task list with dependencies
|
||||
- Check `docs/` for deep dives on specific systems
|
||||
- Check `src/modules/<name>/types.ts` before modifying any module
|
||||
- Always run `npx next build` before committing
|
||||
- Push to `main` → Portainer auto-deploys via Gitea webhook
|
||||
- The 16 docs in `docs/` total ~10,600 lines — search them for architecture questions
|
||||
|
||||
---
|
||||
|
||||
## Documentation Index
|
||||
|
||||
| Doc | Path | Content |
|
||||
| ------------------- | ------------------------------------------ | -------------------------------------------- |
|
||||
| System Architecture | `docs/architecture/SYSTEM-ARCHITECTURE.md` | Overall architecture, module platform design |
|
||||
| Module System | `docs/architecture/MODULE-SYSTEM.md` | Module registry, lifecycle, config format |
|
||||
| Feature Flags | `docs/architecture/FEATURE-FLAGS.md` | Flag system, env overrides |
|
||||
| Storage Layer | `docs/architecture/STORAGE-LAYER.md` | StorageService interface, adapters |
|
||||
| Tagging System | `docs/architecture/TAGGING-SYSTEM.md` | Cross-module tags |
|
||||
| Security & Roles | `docs/architecture/SECURITY-AND-ROLES.md` | Visibility, auth, roles |
|
||||
| Module Dev Guide | `docs/guides/MODULE-DEVELOPMENT.md` | How to create a new module |
|
||||
| HTML Integration | `docs/guides/HTML-TOOL-INTEGRATION.md` | Legacy tool migration |
|
||||
| UI Design System | `docs/guides/UI-DESIGN-SYSTEM.md` | Design tokens, component patterns |
|
||||
| Docker Deployment | `docs/guides/DOCKER-DEPLOYMENT.md` | Full Docker/Portainer/Nginx guide |
|
||||
| Coding Standards | `docs/guides/CODING-STANDARDS.md` | TS strict, naming, patterns |
|
||||
| Testing Strategy | `docs/guides/TESTING-STRATEGY.md` | Testing approach |
|
||||
| Configuration | `docs/guides/CONFIGURATION.md` | Env vars, flags, companies |
|
||||
| Data Model | `docs/DATA-MODEL.md` | All entity schemas |
|
||||
| Repo Structure | `docs/REPO-STRUCTURE.md` | Directory layout |
|
||||
| Prompt Generator | `docs/modules/PROMPT-GENERATOR.md` | Prompt module deep dive |
|
||||
# ArchiTools — Project Context for AI Assistants
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
npm install
|
||||
npm run dev # http://localhost:3000
|
||||
npx next build # verify zero errors before pushing
|
||||
git push origin main # manual redeploy via Portainer UI
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Project Overview
|
||||
|
||||
**ArchiTools** is a modular internal web dashboard for 3 architecture/engineering companies:
|
||||
**Beletage** (architecture), **Urban Switch** (urbanism), **Studii de Teren** (geotechnics).
|
||||
Production: `tools.beletage.ro` — Docker on-premise, Portainer CE, Traefik v3 proxy.
|
||||
|
||||
### Stack
|
||||
|
||||
| Layer | Technology |
|
||||
| ---------- | ------------------------------------------------------- |
|
||||
| Framework | Next.js 16.x, App Router, TypeScript (strict) |
|
||||
| Styling | Tailwind CSS v4, shadcn/ui |
|
||||
| Database | PostgreSQL + PostGIS via Prisma v6 ORM |
|
||||
| Storage | `DatabaseStorageAdapter` (Prisma), localStorage fallback |
|
||||
| Files | MinIO (S3-compatible object storage) |
|
||||
| Auth | NextAuth v4 + Authentik OIDC (auth.beletage.ro) |
|
||||
| Deploy | Docker multi-stage → Portainer CE → Traefik v3 + SSL |
|
||||
| Repo | Gitea at `git.beletage.ro/gitadmin/ArchiTools` |
|
||||
| Language | Code: **English**, UI: **Romanian** |
|
||||
|
||||
### Architecture Principles
|
||||
|
||||
- **Module platform** — each module isolated: own types/services/hooks/components
|
||||
- **Feature flags** gate loading (disabled = zero bundle cost)
|
||||
- **Storage abstraction** via `StorageService` interface + adapters
|
||||
- **Auth via Authentik SSO** — group → role/company mapping
|
||||
- **All entities** include `visibility` / `createdBy` from day one
|
||||
|
||||
---
|
||||
|
||||
## Repository Structure
|
||||
|
||||
```
|
||||
src/
|
||||
├── app/(modules)/ # Route pages (thin wrappers)
|
||||
├── core/ # Platform: auth, storage, flags, tagging, i18n, theme
|
||||
├── modules/<name>/ # Module business logic (see MODULE-MAP.md)
|
||||
│ ├── components/ # UI components
|
||||
│ ├── hooks/ # Module hooks
|
||||
│ ├── services/ # Business logic
|
||||
│ ├── types.ts # Interfaces
|
||||
│ ├── config.ts # Module metadata
|
||||
│ └── index.ts # Public exports
|
||||
├── shared/components/ # ui/ (shadcn), layout/ (sidebar/header), common/
|
||||
├── config/ # modules.ts, flags.ts, navigation.ts, companies.ts
|
||||
docs/ # Architecture, guides, module deep-dives
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Modules (17 total)
|
||||
|
||||
| Module | Route | Key Features |
|
||||
| ------------------ | ------------------- | --------------------------------------------------- |
|
||||
| Dashboard | `/` | KPI cards, activity feed, module grid |
|
||||
| Email Signature | `/email-signature` | Multi-company, live preview, copy/download |
|
||||
| Word XML | `/word-xml` | Category-based XML, simple/advanced, ZIP export |
|
||||
| Registratura | `/registratura` | Registry CRUD, legal deadlines, notifications, NAS |
|
||||
| Tag Manager | `/tag-manager` | Tags CRUD, ManicTime sync |
|
||||
| IT Inventory | `/it-inventory` | Equipment, rack visualization, filters |
|
||||
| Address Book | `/address-book` | Contacts, vCard, Registratura integration |
|
||||
| Password Vault | `/password-vault` | AES-256-GCM encrypted, WiFi QR, multi-user |
|
||||
| Mini Utilities | `/mini-utilities` | 12+ tools: PDF compress, OCR, converters, calc |
|
||||
| Prompt Generator | `/prompt-generator` | 18 templates, text + image targets |
|
||||
| Digital Signatures | `/digital-signatures` | Assets CRUD, file upload, tags |
|
||||
| Word Templates | `/word-templates` | Template library, .docx placeholder detection |
|
||||
| AI Chat | `/ai-chat` | Multi-provider (OpenAI/Claude/Ollama) |
|
||||
| Hot Desk | `/hot-desk` | 4 desks, week calendar, room layout |
|
||||
| ParcelSync | `/parcel-sync` | eTerra ANCPI, PostGIS, enrichment, ePay ordering |
|
||||
| Geoportal | `/geoportal` | MapLibre viewer, parcel search, UAT layers |
|
||||
| Visual CoPilot | `/visual-copilot` | Placeholder — separate repo |
|
||||
|
||||
See `docs/MODULE-MAP.md` for entry points, API routes, and cross-module deps.
|
||||
|
||||
---
|
||||
|
||||
## Development Rules
|
||||
|
||||
### TypeScript Strict Mode Gotchas
|
||||
|
||||
- `arr[0]` is `T | undefined` even after length check — assign to const first
|
||||
- `Record<string, T>[key]` returns `T | undefined` — always null-check
|
||||
- Spread of possibly-undefined: `{ ...obj[key] }` — check existence first
|
||||
- lucide-react Icons: cast through `unknown` → `React.ComponentType<{ className?: string }>`
|
||||
- Prisma `$queryRaw` returns `unknown[]` — cast with `as Array<{ field: type }>`
|
||||
- `?? ""` on `{}` field produces `{}` not `string` — use `typeof` check
|
||||
|
||||
### Conventions
|
||||
|
||||
- **Code**: English | **UI text**: Romanian | **IDs**: uuid v4
|
||||
- **Dates**: ISO strings (`YYYY-MM-DD` display, full ISO timestamps)
|
||||
- **Components**: functional, `'use client'` where needed
|
||||
- **No emojis** in code or UI
|
||||
|
||||
### Storage Performance (CRITICAL)
|
||||
|
||||
- **NEVER** `storage.list()` + `storage.get()` in loop — N+1 bug
|
||||
- **ALWAYS** use `storage.exportAll()` or `storage.export(namespace)` for batch-load
|
||||
- **NEVER** store base64 files in entity JSON — use `lightweight: true` for listing
|
||||
- After mutations: optimistic update OR single `refresh()` — never both
|
||||
|
||||
### Middleware & Large Uploads
|
||||
|
||||
- Middleware buffers entire body — exclude large-upload routes from matcher
|
||||
- Excluded routes: `api/auth|api/notifications/digest|api/compress-pdf|api/address-book|api/projects`
|
||||
- Excluded routes use `requireAuth()` from `auth-check.ts` instead
|
||||
- To add new upload route: (1) exclude from middleware, (2) add `requireAuth()`
|
||||
|
||||
### eTerra / ANCPI Rules
|
||||
|
||||
- ArcGIS: paginate with `resultOffset`/`resultRecordCount` (max 1000)
|
||||
- Sessions expire ~10min — cache TTL 9min, auto-relogin on 401
|
||||
- Health check detects maintenance — block login when down
|
||||
- `WORKSPACE_TO_COUNTY` (42 entries in `county-refresh.ts`) is authoritative
|
||||
- `GisUat.geometry` is huge — always `select` to exclude in list queries
|
||||
- Feature counts cached 5-min TTL
|
||||
- ePay: form-urlencoded body, OpenAM auth, MinIO metadata must be ASCII
|
||||
|
||||
### Before Pushing
|
||||
|
||||
1. `npx next build` — zero errors
|
||||
2. Test on `localhost:3000`
|
||||
3. Commit with descriptive message
|
||||
4. `git push origin main` → manual Portainer redeploy
|
||||
|
||||
---
|
||||
|
||||
## Common Pitfalls (Top 10)
|
||||
|
||||
1. **Middleware body buffering** — upload routes >10MB must be excluded from matcher
|
||||
2. **N+1 storage queries** — use `exportAll()`, never `list()` + `get()` loop
|
||||
3. **GisUat geometry in queries** — exclude with `select`, or 50ms → 5+ seconds
|
||||
4. **Enrichment data loss on re-sync** — upsert must preserve enrichment field
|
||||
5. **Ghostscript corrupts fonts** — use qpdf for PDF compression, never GS
|
||||
6. **eTerra timeout too low** — geometry pages need 60-90s; default 120s
|
||||
7. **Traefik 60s readTimeout** — must set 600s in static config for uploads
|
||||
8. **Portainer CE can't inject env vars** — all env in docker-compose.yml
|
||||
9. **`@prisma/client` in dependencies** (not devDeps) — runtime requirement
|
||||
10. **`output: 'standalone'`** in next.config.ts — required for Docker
|
||||
|
||||
---
|
||||
|
||||
## Infrastructure Quick Reference
|
||||
|
||||
| Service | Address | Purpose |
|
||||
| ----------- | ------------------------ | -------------------------- |
|
||||
| App | 10.10.10.166:3000 | ArchiTools (tools.beletage.ro) |
|
||||
| PostgreSQL | 10.10.10.166:5432 | Database (Prisma) |
|
||||
| MinIO | 10.10.10.166:9002/9003 | Object storage |
|
||||
| Authentik | 10.10.10.166:9100 | SSO (auth.beletage.ro) |
|
||||
| Portainer | 10.10.10.166:9000 | Docker management |
|
||||
| Gitea | 10.10.10.166:3002 | Git (git.beletage.ro) |
|
||||
| Traefik | 10.10.10.199 | Reverse proxy + SSL |
|
||||
| N8N | 10.10.10.166:5678 | Workflow automation |
|
||||
| Stirling PDF | 10.10.10.166:8087 | PDF tools (needs env vars!) |
|
||||
|
||||
## Company IDs
|
||||
|
||||
| ID | Name | Prefix |
|
||||
| ----------------- | --------------- | ------ |
|
||||
| `beletage` | Beletage | B |
|
||||
| `urban-switch` | Urban Switch | US |
|
||||
| `studii-de-teren` | Studii de Teren | SDT |
|
||||
| `group` | Grup | G |
|
||||
|
||||
---
|
||||
|
||||
## Documentation
|
||||
|
||||
| Doc | Path |
|
||||
| ------------------- | ------------------------------------------ |
|
||||
| Module Map | `docs/MODULE-MAP.md` |
|
||||
| Architecture Quick | `docs/ARCHITECTURE-QUICK.md` |
|
||||
| System Architecture | `docs/architecture/SYSTEM-ARCHITECTURE.md` |
|
||||
| Module System | `docs/architecture/MODULE-SYSTEM.md` |
|
||||
| Feature Flags | `docs/architecture/FEATURE-FLAGS.md` |
|
||||
| Storage Layer | `docs/architecture/STORAGE-LAYER.md` |
|
||||
| Security & Roles | `docs/architecture/SECURITY-AND-ROLES.md` |
|
||||
| Module Dev Guide | `docs/guides/MODULE-DEVELOPMENT.md` |
|
||||
| Docker Deployment | `docs/guides/DOCKER-DEPLOYMENT.md` |
|
||||
| Coding Standards | `docs/guides/CODING-STANDARDS.md` |
|
||||
| Data Model | `docs/DATA-MODEL.md` |
|
||||
|
||||
For module-specific deep dives, see `docs/modules/`.
|
||||
|
||||
@@ -51,6 +51,9 @@ services:
|
||||
- ANCPI_LOGIN_URL=https://oassl.ancpi.ro/openam/UI/Login
|
||||
- ANCPI_DEFAULT_SOLICITANT_ID=14452
|
||||
- MINIO_BUCKET_ANCPI=ancpi-documente
|
||||
# Stirling PDF (local PDF tools)
|
||||
- STIRLING_PDF_URL=http://10.10.10.166:8087
|
||||
- STIRLING_PDF_API_KEY=cd829f62-6eef-43eb-a64d-c91af727b53a
|
||||
# iLovePDF cloud compression (free: 250 files/month)
|
||||
- ILOVEPDF_PUBLIC_KEY=${ILOVEPDF_PUBLIC_KEY:-}
|
||||
# Martin vector tile server (geoportal)
|
||||
@@ -65,6 +68,8 @@ services:
|
||||
- NOTIFICATION_FROM_EMAIL=noreply@beletage.ro
|
||||
- NOTIFICATION_FROM_NAME=Alerte Termene
|
||||
- NOTIFICATION_CRON_SECRET=1547a198feca43af6c05622588c6d3b820bad5163b8c20175b2b5bbf8fc1a987
|
||||
# Portal-only users (comma-separated, redirected to /portal)
|
||||
- PORTAL_ONLY_USERS=dtiurbe,d.tiurbe
|
||||
# Address Book API (inter-service auth for external tools)
|
||||
- ADDRESSBOOK_API_KEY=abook-7f3e9a2b4c1d8e5f6a0b3c7d9e2f4a1b5c8d0e3f6a9b2c5d8e1f4a7b0c3d6e
|
||||
depends_on:
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
# ArchiTools — Architecture Quick Reference
|
||||
|
||||
## Data Flow
|
||||
|
||||
```
|
||||
Browser → Traefik (tools.beletage.ro) → Next.js :3000
|
||||
├── App Router (pages)
|
||||
├── API Routes (/api/*)
|
||||
│ ├── Prisma → PostgreSQL + PostGIS
|
||||
│ ├── MinIO (file storage)
|
||||
│ ├── eTerra ANCPI (external GIS API)
|
||||
│ └── Brevo SMTP (email notifications)
|
||||
└── Auth: NextAuth → Authentik OIDC
|
||||
```
|
||||
|
||||
## Module Dependencies
|
||||
|
||||
```
|
||||
registratura ←→ address-book (bidirectional: contacts + reverse lookup)
|
||||
parcel-sync → geoportal (map components reuse)
|
||||
geoportal → PostGIS (spatial queries, vector tiles)
|
||||
parcel-sync → eTerra API (external: ANCPI cadastral data)
|
||||
parcel-sync → ePay API (external: ANCPI CF extract ordering)
|
||||
parcel-sync → MinIO (CF extract PDF storage)
|
||||
notifications → registratura (deadline digest data)
|
||||
all modules → core/storage (KeyValueStore via Prisma)
|
||||
all modules → core/auth (Authentik SSO session)
|
||||
```
|
||||
|
||||
## Critical API Routes (Write Operations)
|
||||
|
||||
| Route | Method | What it does | Auth |
|
||||
| ---------------------------------- | ------ | ----------------------------------- | --------- |
|
||||
| `/api/storage` | PUT/DELETE | KeyValueStore CRUD | Middleware |
|
||||
| `/api/registratura` | POST/PUT/DELETE | Registry entries + audit | Middleware + Bearer |
|
||||
| `/api/registratura/reserved` | POST | Reserve future registry slots | Middleware |
|
||||
| `/api/registratura/debug-sequences`| POST/PATCH | Reset sequence counters | Admin only |
|
||||
| `/api/vault` | PUT/DELETE | Encrypted vault entries | Middleware |
|
||||
| `/api/address-book` | PUT/DELETE | Contact CRUD | Middleware + Bearer |
|
||||
| `/api/eterra/sync-background` | POST | Start GIS sync job | Middleware |
|
||||
| `/api/eterra/uats` | POST/PATCH | UAT management + county refresh | Middleware |
|
||||
| `/api/ancpi/order` | POST | ePay CF extract order | Middleware |
|
||||
| `/api/notifications/digest` | POST | Trigger email digest | Bearer |
|
||||
| `/api/notifications/preferences` | PUT | User notification prefs | Middleware |
|
||||
| `/api/compress-pdf/*` | POST | PDF compression/unlock | requireAuth |
|
||||
|
||||
## Storage Architecture
|
||||
|
||||
```
|
||||
KeyValueStore (Prisma) GisFeature (PostGIS) MinIO
|
||||
├── namespace: module-id ├── layerId + objectId ├── bucket: tools
|
||||
├── key: entity UUID ├── geometry (GeoJSON) ├── bucket: ancpi-cf
|
||||
└── value: JSON blob ├── enrichment (JSONB) └── PDF files
|
||||
└── geom (native PostGIS)
|
||||
```
|
||||
|
||||
## Auth Flow
|
||||
|
||||
```
|
||||
User → /auth/signin → Authentik OIDC → callback → NextAuth session
|
||||
├── Middleware: checks JWT token, redirects if unauthenticated
|
||||
├── Portal-only users: env PORTAL_ONLY_USERS → redirected to /portal
|
||||
└── API routes excluded from middleware: use requireAuth() or Bearer token
|
||||
```
|
||||
|
||||
## Environment Variables (Critical)
|
||||
|
||||
| Var | Required | Used by |
|
||||
| ---------------------- | -------- | -------------------------- |
|
||||
| `DATABASE_URL` | Yes | Prisma |
|
||||
| `NEXTAUTH_SECRET` | Yes | NextAuth JWT |
|
||||
| `NEXTAUTH_URL` | Yes | Auth redirects |
|
||||
| `ENCRYPTION_SECRET` | Yes | Password Vault AES-256 |
|
||||
| `STIRLING_PDF_URL` | Yes | PDF compression/unlock |
|
||||
| `STIRLING_PDF_API_KEY` | Yes | Stirling PDF auth |
|
||||
| `NOTIFICATION_CRON_SECRET` | Yes | Digest endpoint Bearer |
|
||||
| `MINIO_*` | Yes | MinIO connection |
|
||||
| `ANCPI_*` | For ePay | ePay CF ordering |
|
||||
| `ILOVEPDF_PUBLIC_KEY` | Optional | Cloud PDF compression |
|
||||
| `PORTAL_ONLY_USERS` | Optional | Comma-separated usernames |
|
||||
@@ -0,0 +1,140 @@
|
||||
# ArchiTools — Module Map
|
||||
|
||||
Quick reference: entry points, key files, API routes, and cross-module dependencies.
|
||||
|
||||
## Module Index
|
||||
|
||||
| Module | Entry Point | Config | Types |
|
||||
| ------ | ----------- | ------ | ----- |
|
||||
| [Dashboard](#dashboard) | `modules/dashboard/index.ts` | — | `types.ts` |
|
||||
| [Email Signature](#email-signature) | `modules/email-signature/index.ts` | `config.ts` | `types.ts` |
|
||||
| [Word XML](#word-xml) | `modules/word-xml/index.ts` | `config.ts` | `types.ts` |
|
||||
| [Registratura](#registratura) | `modules/registratura/index.ts` | `config.ts` | `types.ts` |
|
||||
| [Tag Manager](#tag-manager) | `modules/tag-manager/index.ts` | `config.ts` | `types.ts` |
|
||||
| [IT Inventory](#it-inventory) | `modules/it-inventory/index.ts` | `config.ts` | `types.ts` |
|
||||
| [Address Book](#address-book) | `modules/address-book/index.ts` | `config.ts` | `types.ts` |
|
||||
| [Password Vault](#password-vault) | `modules/password-vault/index.ts` | `config.ts` | `types.ts` |
|
||||
| [Mini Utilities](#mini-utilities) | `modules/mini-utilities/index.ts` | `config.ts` | `types.ts` |
|
||||
| [Prompt Generator](#prompt-generator) | `modules/prompt-generator/index.ts` | `config.ts` | `types.ts` |
|
||||
| [Digital Signatures](#digital-signatures) | `modules/digital-signatures/index.ts` | `config.ts` | `types.ts` |
|
||||
| [Word Templates](#word-templates) | `modules/word-templates/index.ts` | `config.ts` | `types.ts` |
|
||||
| [AI Chat](#ai-chat) | `modules/ai-chat/index.ts` | `config.ts` | `types.ts` |
|
||||
| [Hot Desk](#hot-desk) | `modules/hot-desk/index.ts` | `config.ts` | `types.ts` |
|
||||
| [ParcelSync](#parcel-sync) | `modules/parcel-sync/index.ts` | `config.ts` | `types.ts` |
|
||||
| [Geoportal](#geoportal) | `modules/geoportal/index.ts` | `config.ts` | `types.ts` |
|
||||
| [Visual CoPilot](#visual-copilot) | `modules/visual-copilot/index.ts` | `config.ts` | — |
|
||||
|
||||
---
|
||||
|
||||
## Module Details
|
||||
|
||||
### Dashboard
|
||||
- **Route**: `/`
|
||||
- **Main component**: `app/(modules)/page.tsx` (home page, not a registered module)
|
||||
- **API routes**: none (reads via storage API)
|
||||
- **Cross-deps**: none
|
||||
|
||||
### Email Signature
|
||||
- **Route**: `/email-signature`
|
||||
- **Main component**: `components/email-signature-module.tsx`
|
||||
- **API routes**: none (client-only)
|
||||
- **Cross-deps**: none
|
||||
|
||||
### Word XML
|
||||
- **Route**: `/word-xml`
|
||||
- **Main component**: `components/word-xml-module.tsx`
|
||||
- **Services**: `services/xml-builder.ts`, `services/zip-export.ts`
|
||||
- **API routes**: none (client-only)
|
||||
- **Cross-deps**: none
|
||||
|
||||
### Registratura
|
||||
- **Route**: `/registratura`
|
||||
- **Main component**: `components/registratura-module.tsx`
|
||||
- **Key services**: `services/registry-service.ts` (numbering, advisory locks), `services/working-days.ts` (Romanian holidays), `services/deadline-catalog.ts` (18 legal deadline types), `services/deadline-service.ts`
|
||||
- **API routes**: `/api/registratura` (CRUD + audit), `/api/registratura/reserved`, `/api/registratura/debug-sequences`, `/api/registratura/audit`, `/api/registratura/status-check`
|
||||
- **Cross-deps**: **address-book** (quick contact, reverse lookup), **notifications** (deadline digest)
|
||||
|
||||
### Tag Manager
|
||||
- **Route**: `/tag-manager`
|
||||
- **Main component**: `components/tag-manager-module.tsx`
|
||||
- **Services**: `services/manictime-sync.ts`
|
||||
- **API routes**: `/api/manictime`
|
||||
- **Cross-deps**: core/tagging
|
||||
|
||||
### IT Inventory
|
||||
- **Route**: `/it-inventory`
|
||||
- **Main component**: `components/it-inventory-module.tsx`
|
||||
- **API routes**: none (via storage API)
|
||||
- **Cross-deps**: none
|
||||
|
||||
### Address Book
|
||||
- **Route**: `/address-book`
|
||||
- **Main component**: `components/address-book-module.tsx`
|
||||
- **Services**: `services/vcard-export.ts`
|
||||
- **API routes**: `/api/address-book` (CRUD, Bearer token support)
|
||||
- **Cross-deps**: **registratura** (reverse lookup via `useRegistry`)
|
||||
|
||||
### Password Vault
|
||||
- **Route**: `/password-vault`
|
||||
- **Main component**: `components/password-vault-module.tsx`
|
||||
- **API routes**: `/api/vault` (AES-256-GCM encrypt/decrypt)
|
||||
- **Cross-deps**: none
|
||||
|
||||
### Mini Utilities
|
||||
- **Route**: `/mini-utilities`
|
||||
- **Main component**: `components/mini-utilities-module.tsx` (monolithic, tab-based)
|
||||
- **API routes**: `/api/compress-pdf/*` (local qpdf + cloud iLovePDF), `/api/compress-pdf/unlock`
|
||||
- **Cross-deps**: none
|
||||
|
||||
### Prompt Generator
|
||||
- **Route**: `/prompt-generator`
|
||||
- **Main component**: `components/prompt-generator-module.tsx`
|
||||
- **Services**: `services/prompt-templates.ts` (18 templates)
|
||||
- **API routes**: none (client-only)
|
||||
- **Cross-deps**: none
|
||||
|
||||
### Digital Signatures
|
||||
- **Route**: `/digital-signatures`
|
||||
- **Main component**: `components/digital-signatures-module.tsx`
|
||||
- **API routes**: none (via storage API)
|
||||
- **Cross-deps**: none
|
||||
|
||||
### Word Templates
|
||||
- **Route**: `/word-templates`
|
||||
- **Main component**: `components/word-templates-module.tsx`
|
||||
- **Services**: `services/docx-analyzer.ts`
|
||||
- **API routes**: none (via storage API)
|
||||
- **Cross-deps**: none
|
||||
|
||||
### AI Chat
|
||||
- **Route**: `/ai-chat`
|
||||
- **Main component**: `components/ai-chat-module.tsx`
|
||||
- **API routes**: `/api/ai-chat` (multi-provider proxy)
|
||||
- **Cross-deps**: tag-manager (project linking)
|
||||
|
||||
### Hot Desk
|
||||
- **Route**: `/hot-desk`
|
||||
- **Main component**: `components/hot-desk-module.tsx`
|
||||
- **Services**: `services/desk-layout.ts`
|
||||
- **API routes**: none (via storage API)
|
||||
- **Cross-deps**: none
|
||||
|
||||
### ParcelSync
|
||||
- **Route**: `/parcel-sync`
|
||||
- **Main component**: `components/parcel-sync-module.tsx` (~4100 lines, 5 tabs)
|
||||
- **Key services**: `services/eterra-client.ts` (~1000 lines, eTerra API), `services/sync-service.ts`, `services/enrich-service.ts`, `services/eterra-health.ts`, `services/epay-client.ts`, `services/epay-queue.ts`, `services/epay-storage.ts`, `services/no-geom-sync.ts`
|
||||
- **API routes**: `/api/eterra/*` (login, sync, search, features, UATs, health), `/api/ancpi/*` (order, test), `/api/geoportal/*` (search, boundaries, setup)
|
||||
- **Cross-deps**: **geoportal** (map components via map-tab.tsx), **MinIO** (CF extract PDFs), **PostGIS** (GisFeature, GisUat)
|
||||
|
||||
### Geoportal
|
||||
- **Route**: `/geoportal`
|
||||
- **Main component**: `components/geoportal-module.tsx`
|
||||
- **Key components**: `components/map-viewer.tsx` (MapLibre), `components/basemap-switcher.tsx`, `components/selection-toolbar.tsx`, `components/feature-info-panel.tsx`
|
||||
- **API routes**: `/api/geoportal/*` (search, boundary-check, uat-bounds, setup-views)
|
||||
- **Cross-deps**: **parcel-sync** (declared dependency — uses PostGIS data)
|
||||
|
||||
### Visual CoPilot
|
||||
- **Route**: `/visual-copilot`
|
||||
- **Status**: Placeholder (iframe to separate repo `git.beletage.ro/gitadmin/vim`)
|
||||
- **API routes**: none
|
||||
- **Cross-deps**: none
|
||||
@@ -1,456 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ro">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Configurator semnatura e-mail</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;700&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
body { font-family: 'Inter', sans-serif; }
|
||||
.no-select { -webkit-user-select: none; -ms-user-select: none; user-select: none; }
|
||||
|
||||
input[type=range] {
|
||||
-webkit-appearance: none; appearance: none; width: 100%; height: 4px;
|
||||
background: #e5e7eb; border-radius: 5px; outline: none; transition: background 0.2s ease;
|
||||
}
|
||||
input[type=range]:hover { background: #d1d5db; }
|
||||
input[type=range]::-webkit-slider-thumb {
|
||||
-webkit-appearance: none; appearance: none; width: 12px; height: 20px;
|
||||
background: #22B5AB; cursor: pointer; border-radius: 4px;
|
||||
margin-top: -8px; box-shadow: 0 1px 3px rgba(0,0,0,0.2);
|
||||
transition: transform 0.1s ease-in-out, box-shadow 0.1s ease-in-out;
|
||||
}
|
||||
input[type=range]::-webkit-slider-thumb:active { transform: scale(1.15); box-shadow: 0 2px 6px rgba(0,0,0,0.3); }
|
||||
input[type=range]::-moz-range-thumb {
|
||||
width: 12px; height: 20px; background: #22B5AB; cursor: pointer;
|
||||
border-radius: 4px; border: none; box-shadow: 0 1px 3px rgba(0,0,0,0.2);
|
||||
}
|
||||
#preview-wrapper { transition: transform 0.2s ease-in-out; transform-origin: top left; }
|
||||
.color-swatch {
|
||||
width: 24px; height: 24px; border-radius: 9999px; cursor: pointer;
|
||||
border: 2px solid transparent; transition: all 0.2s ease;
|
||||
}
|
||||
.color-swatch.active { border-color: #22B5AB; transform: scale(1.1); box-shadow: 0 0 0 2px white, 0 0 0 4px #22B5AB; }
|
||||
.collapsible-content { max-height: 0; overflow: hidden; transition: max-height 0.3s ease-out; }
|
||||
.collapsible-content.open { max-height: 1000px; /* Valoare mare pentru a permite extinderea */ }
|
||||
.collapsible-trigger svg { transition: transform 0.3s ease; }
|
||||
.collapsible-trigger.open svg { transform: rotate(90deg); }
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-gray-50 text-gray-800 no-select">
|
||||
|
||||
<div class="container mx-auto p-4 md:p-8">
|
||||
<header class="text-center mb-10">
|
||||
<h1 class="text-3xl md:text-4xl font-bold text-gray-900">Configurator semnatura e-mail</h1>
|
||||
</header>
|
||||
|
||||
<div class="flex flex-col lg:flex-row gap-8">
|
||||
|
||||
<!-- Panoul de control -->
|
||||
<aside class="lg:w-2/5 bg-white p-6 rounded-2xl shadow-lg border border-gray-200">
|
||||
<div id="controls">
|
||||
<!-- Secțiunea Date Personale -->
|
||||
<div class="mb-4">
|
||||
<h3 class="text-lg font-semibold text-gray-800 border-b pb-2 mb-4">Date Personale</h3>
|
||||
<div class="space-y-3">
|
||||
<div>
|
||||
<label for="input-prefix" class="block text-sm font-medium text-gray-700 mb-1">Titulatură (prefix)</label>
|
||||
<input type="text" id="input-prefix" value="arh." class="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:ring-teal-500 focus:border-teal-500">
|
||||
</div>
|
||||
<div>
|
||||
<label for="input-name" class="block text-sm font-medium text-gray-700 mb-1">Nume și Prenume</label>
|
||||
<input type="text" id="input-name" value="Marius TĂRĂU" class="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:ring-teal-500 focus:border-teal-500">
|
||||
</div>
|
||||
<div>
|
||||
<label for="input-title" class="block text-sm font-medium text-gray-700 mb-1">Funcția</label>
|
||||
<input type="text" id="input-title" value="Arhitect • Beletage SRL" class="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:ring-teal-500 focus:border-teal-500">
|
||||
</div>
|
||||
<div>
|
||||
<label for="input-phone" class="block text-sm font-medium text-gray-700 mb-1">Telefon (format 07xxxxxxxx)</label>
|
||||
<input type="tel" id="input-phone" value="0785123433" class="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:ring-teal-500 focus:border-teal-500">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Culori Text (Collapsible) -->
|
||||
<div class="mb-4">
|
||||
<div class="collapsible-trigger flex justify-between items-center cursor-pointer border-b pb-2 mb-2">
|
||||
<h3 class="text-lg font-semibold text-gray-800">Culori Text</h3>
|
||||
<svg class="w-4 h-4 text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path></svg>
|
||||
</div>
|
||||
<div class="collapsible-content">
|
||||
<div id="color-controls" class="space-y-2 pt-2"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Secțiunea Stil & Aranjare (Collapsible) -->
|
||||
<div class="mb-4">
|
||||
<div class="collapsible-trigger flex justify-between items-center cursor-pointer border-b pb-2 mb-2">
|
||||
<h3 class="text-lg font-semibold text-gray-800">Stil & Aranjare</h3>
|
||||
<svg class="w-4 h-4 text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path></svg>
|
||||
</div>
|
||||
<div class="collapsible-content">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-x-6 gap-y-3 pt-2">
|
||||
<div>
|
||||
<label for="green-line-width" class="block text-sm font-medium text-gray-700 mb-2">Lungime linie verde (<span id="green-line-value">97</span>px)</label>
|
||||
<input id="green-line-width" type="range" min="50" max="300" value="97">
|
||||
</div>
|
||||
<div>
|
||||
<label for="section-spacing" class="block text-sm font-medium text-gray-700 mb-2">Spațiere vert. secțiuni (<span id="section-spacing-value">10</span>px)</label>
|
||||
<input id="section-spacing" type="range" min="0" max="30" value="10">
|
||||
</div>
|
||||
<div>
|
||||
<label for="logo-spacing" class="block text-sm font-medium text-gray-700 mb-2">Spațiere vert. Logo (<span id="logo-spacing-value">10</span>px)</label>
|
||||
<input id="logo-spacing" type="range" min="0" max="30" value="10">
|
||||
</div>
|
||||
<div>
|
||||
<label for="title-spacing" class="block text-sm font-medium text-gray-700 mb-2">Spațiere vert. funcție (<span id="title-spacing-value">2</span>px)</label>
|
||||
<input id="title-spacing" type="range" min="0" max="20" value="2">
|
||||
</div>
|
||||
<div>
|
||||
<label for="b-gutter-width" class="block text-sm font-medium text-gray-700 mb-2">Aliniere contact (<span id="b-gutter-value">13</span>px)</label>
|
||||
<input id="b-gutter-width" type="range" min="0" max="150" value="13">
|
||||
</div>
|
||||
<div>
|
||||
<label for="icon-text-spacing" class="block text-sm font-medium text-gray-700 mb-2">Spațiu Icon-Text (<span id="icon-text-spacing-value">5</span>px)</label>
|
||||
<input id="icon-text-spacing" type="range" min="-10" max="30" value="5">
|
||||
</div>
|
||||
<div>
|
||||
<label for="icon-vertical-pos" class="block text-sm font-medium text-gray-700 mb-2">Aliniere vert. iconițe (<span id="icon-vertical-value">1</span>px)</label>
|
||||
<input id="icon-vertical-pos" type="range" min="-10" max="10" value="1">
|
||||
</div>
|
||||
<div>
|
||||
<label for="motto-spacing" class="block text-sm font-medium text-gray-700 mb-2">Spațiere vert. motto (<span id="motto-spacing-value">3</span>px)</label>
|
||||
<input id="motto-spacing" type="range" min="0" max="20" value="3">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Opțiuni -->
|
||||
<div class="mb-4">
|
||||
<h3 class="text-lg font-semibold text-gray-800 border-b pb-2 mb-4">Opțiuni</h3>
|
||||
<div class="space-y-2">
|
||||
<label class="flex items-center space-x-3 cursor-pointer">
|
||||
<input type="checkbox" id="reply-variant-checkbox" class="h-4 w-4 rounded border-gray-300 text-teal-600 focus:ring-teal-500">
|
||||
<span class="text-sm font-medium text-gray-700">Variantă simplă (fără logo/adresă)</span>
|
||||
</label>
|
||||
<label class="flex items-center space-x-3 cursor-pointer">
|
||||
<input type="checkbox" id="super-reply-variant-checkbox" class="h-4 w-4 rounded border-gray-300 text-teal-600 focus:ring-teal-500">
|
||||
<span class="text-sm font-medium text-gray-700">Super-simplă (doar nume/telefon)</span>
|
||||
</label>
|
||||
<label class="flex items-center space-x-3 cursor-pointer">
|
||||
<input type="checkbox" id="use-svg-checkbox" class="h-4 w-4 rounded border-gray-300 text-teal-600 focus:ring-teal-500">
|
||||
<span class="text-sm font-medium text-gray-700">Folosește imagini SVG (calitate maximă)</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Buton de Export -->
|
||||
<div class="mt-8 pt-6 border-t">
|
||||
<button id="export-btn" class="w-full bg-teal-600 text-white font-bold py-3 px-4 rounded-lg hover:bg-teal-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-teal-500 transition-all duration-300 ease-in-out transform hover:scale-105">
|
||||
Descarcă HTML
|
||||
</button>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- Previzualizare Live -->
|
||||
<main class="lg:w-3/5 bg-white p-6 rounded-2xl shadow-lg border border-gray-200 overflow-hidden">
|
||||
<div class="flex justify-between items-center border-b pb-3 mb-4">
|
||||
<h2 class="text-2xl font-bold text-gray-900">Previzualizare Live</h2>
|
||||
<button id="zoom-btn" class="text-sm bg-gray-200 text-gray-700 px-3 py-1 rounded-md hover:bg-gray-300">Zoom 100%</button>
|
||||
</div>
|
||||
<div id="preview-wrapper" class="overflow-auto">
|
||||
<div id="preview-container">
|
||||
<!-- Aici este inserat codul HTML al semnăturii -->
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const controls = {
|
||||
prefix: document.getElementById('input-prefix'),
|
||||
name: document.getElementById('input-name'),
|
||||
title: document.getElementById('input-title'),
|
||||
phone: document.getElementById('input-phone'),
|
||||
greenLine: document.getElementById('green-line-width'),
|
||||
gutter: document.getElementById('b-gutter-width'),
|
||||
iconTextSpacing: document.getElementById('icon-text-spacing'),
|
||||
iconVertical: document.getElementById('icon-vertical-pos'),
|
||||
mottoSpacing: document.getElementById('motto-spacing'),
|
||||
sectionSpacing: document.getElementById('section-spacing'),
|
||||
titleSpacing: document.getElementById('title-spacing'),
|
||||
logoSpacing: document.getElementById('logo-spacing'),
|
||||
replyCheckbox: document.getElementById('reply-variant-checkbox'),
|
||||
superReplyCheckbox: document.getElementById('super-reply-variant-checkbox'),
|
||||
useSvgCheckbox: document.getElementById('use-svg-checkbox'),
|
||||
exportBtn: document.getElementById('export-btn'),
|
||||
zoomBtn: document.getElementById('zoom-btn'),
|
||||
colorControls: document.getElementById('color-controls')
|
||||
};
|
||||
|
||||
const values = {
|
||||
greenLine: document.getElementById('green-line-value'),
|
||||
gutter: document.getElementById('b-gutter-value'),
|
||||
iconTextSpacing: document.getElementById('icon-text-spacing-value'),
|
||||
iconVertical: document.getElementById('icon-vertical-value'),
|
||||
mottoSpacing: document.getElementById('motto-spacing-value'),
|
||||
sectionSpacing: document.getElementById('section-spacing-value'),
|
||||
titleSpacing: document.getElementById('title-spacing-value'),
|
||||
logoSpacing: document.getElementById('logo-spacing-value')
|
||||
};
|
||||
|
||||
const previewContainer = document.getElementById('preview-container');
|
||||
const previewWrapper = document.getElementById('preview-wrapper');
|
||||
|
||||
const imageSets = {
|
||||
png: {
|
||||
logo: 'https://beletage.ro/img/Semnatura-Logo.png',
|
||||
greySlash: 'https://beletage.ro/img/Grey-slash.png',
|
||||
greenSlash: 'https://beletage.ro/img/Green-slash.png'
|
||||
},
|
||||
svg: {
|
||||
logo: 'https://beletage.ro/img/Logo-Beletage.svg',
|
||||
greySlash: 'https://beletage.ro/img/Grey-slash.svg',
|
||||
greenSlash: 'https://beletage.ro/img/Green-slash.svg'
|
||||
}
|
||||
};
|
||||
|
||||
const beletageColors = {
|
||||
verde: '#22B5AB',
|
||||
griInchis: '#54504F',
|
||||
griDeschis: '#A7A9AA',
|
||||
negru: '#323232'
|
||||
};
|
||||
|
||||
const colorConfig = {
|
||||
prefix: { label: 'Titulatură', default: beletageColors.griInchis },
|
||||
name: { label: 'Nume', default: beletageColors.griInchis },
|
||||
title: { label: 'Funcție', default: beletageColors.griDeschis },
|
||||
address: { label: 'Adresă', default: beletageColors.griDeschis },
|
||||
phone: { label: 'Telefon', default: beletageColors.griInchis },
|
||||
website: { label: 'Website', default: beletageColors.griInchis },
|
||||
motto: { label: 'Motto', default: beletageColors.verde }
|
||||
};
|
||||
|
||||
let currentColors = {};
|
||||
|
||||
function createColorPickers() {
|
||||
for (const [key, config] of Object.entries(colorConfig)) {
|
||||
currentColors[key] = config.default;
|
||||
const controlRow = document.createElement('div');
|
||||
controlRow.className = 'flex items-center justify-between';
|
||||
const label = document.createElement('span');
|
||||
label.className = 'text-sm font-medium text-gray-700';
|
||||
label.textContent = config.label;
|
||||
controlRow.appendChild(label);
|
||||
const swatchesContainer = document.createElement('div');
|
||||
swatchesContainer.className = 'flex items-center space-x-2';
|
||||
swatchesContainer.dataset.controlKey = key;
|
||||
for (const color of Object.values(beletageColors)) {
|
||||
const swatch = document.createElement('div');
|
||||
swatch.className = 'color-swatch';
|
||||
swatch.style.backgroundColor = color;
|
||||
swatch.dataset.color = color;
|
||||
if (color === config.default) swatch.classList.add('active');
|
||||
swatchesContainer.appendChild(swatch);
|
||||
}
|
||||
controlRow.appendChild(swatchesContainer);
|
||||
controls.colorControls.appendChild(controlRow);
|
||||
}
|
||||
|
||||
controls.colorControls.addEventListener('click', (e) => {
|
||||
if (e.target.classList.contains('color-swatch')) {
|
||||
const key = e.target.parentElement.dataset.controlKey;
|
||||
currentColors[key] = e.target.dataset.color;
|
||||
e.target.parentElement.querySelectorAll('.color-swatch').forEach(s => s.classList.remove('active'));
|
||||
e.target.classList.add('active');
|
||||
updatePreview();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function generateSignatureHTML(data) {
|
||||
const {
|
||||
prefix, name, title, phone, phoneLink, greenLineWidth, gutterWidth,
|
||||
iconTextSpacing, iconVerticalOffset, mottoSpacing, sectionSpacing, titleSpacing, logoSpacing,
|
||||
isReply, isSuperReply, colors, images
|
||||
} = data;
|
||||
|
||||
const hideTitle = isReply || isSuperReply ? 'mso-hide:all;display:none!important;max-height:0;overflow:hidden;font-size:0;line-height:0;' : '';
|
||||
const hideLogoAddress = isReply || isSuperReply ? 'mso-hide:all;display:none!important;max-height:0;overflow:hidden;font-size:0;line-height:0;' : '';
|
||||
const hideBottom = isSuperReply ? 'mso-hide:all;display:none!important;max-height:0;overflow:hidden;font-size:0;line-height:0;' : '';
|
||||
const hidePhoneIcon = isSuperReply ? 'mso-hide:all;display:none!important;max-height:0;overflow:hidden;font-size:0;line-height:0;' : '';
|
||||
|
||||
const spacerWidth = Math.max(0, iconTextSpacing);
|
||||
const textPaddingLeft = Math.max(0, -iconTextSpacing);
|
||||
|
||||
const prefixHTML = prefix ? `<span style="font-size:13px; color:${colors.prefix};">${prefix} </span>` : '';
|
||||
const logoWidth = controls.useSvgCheckbox.checked ? 162 : 162;
|
||||
const logoHeight = controls.useSvgCheckbox.checked ? 24 : 24;
|
||||
|
||||
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>
|
||||
<tr><td style="padding:0 0 ${titleSpacing}px 0;">${prefixHTML}<span style="font-size:15px; color:${colors.name}; font-weight:700;">${name}</span></td></tr>
|
||||
<tr style="${hideTitle}"><td style="padding:0 0 8px 0;"><span style="font-size:12px; color:${colors.title};">${title}</span></td></tr>
|
||||
<tr style="${hideBottom}">
|
||||
<td style="padding:0; font-size:0; line-height:0;">
|
||||
<table role="presentation" cellpadding="0" cellspacing="0" border="0" width="540">
|
||||
<tr>
|
||||
<td width="${greenLineWidth}" height="2" bgcolor="${beletageColors.verde}" style="font-size:0; line-height:0; height:2px;"></td>
|
||||
<td width="${540 - greenLineWidth}" height="2" style="font-size:0; line-height:0; height:2px;"></td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
<tr style="${hideLogoAddress}"><td style="padding:${logoSpacing}px 0 ${parseInt(logoSpacing, 10) + 2}px 0;">
|
||||
<a href="https://www.beletage.ro" style="text-decoration:none; border:0;">
|
||||
<img src="${images.logo}" alt="Beletage" style="display:block; border:0; height:${logoHeight}px; width:${logoWidth}px;" height="${logoHeight}" width="${logoWidth}">
|
||||
</a>
|
||||
</td></tr>
|
||||
<tr>
|
||||
<td style="padding-top:${hideLogoAddress ? '0' : sectionSpacing}px;">
|
||||
<table role="presentation" cellpadding="0" cellspacing="0" border="0" width="540" style="font-size:13px; line-height:18px;">
|
||||
<tbody>
|
||||
<tr style="${hideLogoAddress}">
|
||||
<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;">
|
||||
<img src="${images.greySlash}" alt="" width="11" height="11" style="display: block; border:0;">
|
||||
</td>
|
||||
<td width="${spacerWidth}" style="width:${spacerWidth}px; font-size:0; line-height:0;"></td>
|
||||
<td style="vertical-align:top; padding:0 0 0 ${textPaddingLeft}px;">
|
||||
<a href="https://maps.google.com/?q=str.%20Unirii%203%2C%20ap.%2026%2C%20Cluj-Napoca%20400417%2C%20Rom%C3%A2nia" style="color:${colors.address}; text-decoration:none;"><span style="color:${colors.address}; text-decoration:none;">str. Unirii, nr. 3, ap. 26<br>Cluj-Napoca, Cluj 400417<br>România</span></a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="${gutterWidth}" style="width:${gutterWidth}px; font-size:0; line-height:0;"></td>
|
||||
<td width="11" style="width:11px; vertical-align:top; padding-top:${12 + iconVerticalOffset}px; ${hidePhoneIcon}">
|
||||
<img src="${images.greenSlash}" alt="" width="11" height="7" style="display: block; border:0;">
|
||||
</td>
|
||||
<td width="${isSuperReply ? 0 : spacerWidth}" style="width:${isSuperReply ? 0 : spacerWidth}px; font-size:0; line-height:0;"></td>
|
||||
<td style="vertical-align:top; padding:8px 0 0 ${isSuperReply ? 0 : textPaddingLeft}px;">
|
||||
<a href="${phoneLink}" style="color:${colors.phone}; text-decoration:none;"><span style="color:${colors.phone}; text-decoration:none;">${phone}</span></a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
<tr style="${hideBottom}"><td style="padding:${sectionSpacing}px 0 ${mottoSpacing}px 0;"><a href="https://www.beletage.ro" style="color:${colors.website}; text-decoration:none;"><span style="color:${colors.website}; text-decoration:none;">www.beletage.ro</span></a></td></tr>
|
||||
<tr style="${hideBottom}">
|
||||
<td style="padding:0; font-size:0; line-height:0;">
|
||||
<table role="presentation" cellpadding="0" cellspacing="0" border="0" width="540">
|
||||
<tr>
|
||||
<td width="${greenLineWidth}" height="1" bgcolor="${beletageColors.verde}" style="font-size:0; line-height:0; height:1px;"></td>
|
||||
<td width="${540 - greenLineWidth}" height="1" style="font-size:0; line-height:0; height:1px;"></td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
<tr style="${hideBottom}"><td style="padding:${mottoSpacing}px 0 0 0;"><span style="font-size:12px; color:${colors.motto}; font-style:italic;">we make complex simple</span></td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
`;
|
||||
}
|
||||
|
||||
function updatePreview() {
|
||||
const phoneRaw = controls.phone.value.replace(/\s/g, '');
|
||||
let formattedPhone = controls.phone.value;
|
||||
let phoneLink = `tel:${phoneRaw}`;
|
||||
|
||||
if (phoneRaw.length === 10 && phoneRaw.startsWith('07')) {
|
||||
formattedPhone = `+40 ${phoneRaw.substring(1, 4)} ${phoneRaw.substring(4, 7)} ${phoneRaw.substring(7, 10)}`;
|
||||
phoneLink = `tel:+40${phoneRaw.substring(1)}`;
|
||||
}
|
||||
|
||||
if (controls.superReplyCheckbox.checked) {
|
||||
controls.replyCheckbox.checked = true;
|
||||
controls.replyCheckbox.disabled = true;
|
||||
} else {
|
||||
controls.replyCheckbox.disabled = false;
|
||||
}
|
||||
|
||||
const data = {
|
||||
prefix: controls.prefix.value,
|
||||
name: controls.name.value,
|
||||
title: controls.title.value,
|
||||
phone: formattedPhone,
|
||||
phoneLink: phoneLink,
|
||||
greenLineWidth: controls.greenLine.value,
|
||||
gutterWidth: controls.gutter.value,
|
||||
iconTextSpacing: controls.iconTextSpacing.value,
|
||||
iconVerticalOffset: parseInt(controls.iconVertical.value, 10),
|
||||
mottoSpacing: controls.mottoSpacing.value,
|
||||
sectionSpacing: controls.sectionSpacing.value,
|
||||
titleSpacing: controls.titleSpacing.value,
|
||||
logoSpacing: controls.logoSpacing.value,
|
||||
isReply: controls.replyCheckbox.checked,
|
||||
isSuperReply: controls.superReplyCheckbox.checked,
|
||||
colors: { ...currentColors },
|
||||
images: controls.useSvgCheckbox.checked ? imageSets.svg : imageSets.png
|
||||
};
|
||||
|
||||
values.greenLine.textContent = data.greenLineWidth;
|
||||
values.gutter.textContent = data.gutterWidth;
|
||||
values.iconTextSpacing.textContent = data.iconTextSpacing;
|
||||
values.iconVertical.textContent = data.iconVerticalOffset;
|
||||
values.mottoSpacing.textContent = data.mottoSpacing;
|
||||
values.sectionSpacing.textContent = data.sectionSpacing;
|
||||
values.titleSpacing.textContent = data.titleSpacing;
|
||||
values.logoSpacing.textContent = data.logoSpacing;
|
||||
|
||||
previewContainer.innerHTML = generateSignatureHTML(data);
|
||||
}
|
||||
|
||||
// --- Inițializare ---
|
||||
createColorPickers();
|
||||
|
||||
Object.values(controls).forEach(control => {
|
||||
if (control.id !== 'export-btn' && control.id !== 'zoom-btn') {
|
||||
control.addEventListener('input', updatePreview);
|
||||
}
|
||||
});
|
||||
|
||||
document.querySelectorAll('.collapsible-trigger').forEach(trigger => {
|
||||
trigger.addEventListener('click', () => {
|
||||
const content = trigger.nextElementSibling;
|
||||
trigger.classList.toggle('open');
|
||||
content.classList.toggle('open');
|
||||
});
|
||||
});
|
||||
|
||||
controls.zoomBtn.addEventListener('click', () => {
|
||||
const isZoomed = previewWrapper.style.transform === 'scale(2)';
|
||||
if (isZoomed) {
|
||||
previewWrapper.style.transform = 'scale(1)';
|
||||
controls.zoomBtn.textContent = 'Zoom 200%';
|
||||
} else {
|
||||
previewWrapper.style.transform = 'scale(2)';
|
||||
controls.zoomBtn.textContent = 'Zoom 100%';
|
||||
}
|
||||
});
|
||||
|
||||
controls.exportBtn.addEventListener('click', () => {
|
||||
const finalHTML = previewContainer.innerHTML;
|
||||
const blob = new Blob([finalHTML], { type: 'text/html' });
|
||||
const a = document.createElement('a');
|
||||
a.href = URL.createObjectURL(blob);
|
||||
a.download = 'semnatura-beletage.html';
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
});
|
||||
|
||||
updatePreview();
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,148 +0,0 @@
|
||||
Pauza de masa
|
||||
Timp personal
|
||||
|
||||
Concediu
|
||||
Compensare overtime
|
||||
|
||||
Beletage
|
||||
Ofertare
|
||||
Configurari
|
||||
Organizare initiala
|
||||
Pregatire Portofoliu
|
||||
Website
|
||||
Documentare
|
||||
Design grafic
|
||||
Design interior
|
||||
Design exterior
|
||||
|
||||
Releveu
|
||||
Reclama
|
||||
|
||||
000 Farmacie
|
||||
002 Cladire birouri Stratec
|
||||
003 PUZ Bellavista
|
||||
007 Design Apartament Teodora
|
||||
010 Casa Doinei
|
||||
016 Duplex Eremia
|
||||
024 Bloc Petofi
|
||||
028 PUZ Borhanci-Sopor
|
||||
033 Mansardare Branului
|
||||
039 Cabinete Stoma Scala
|
||||
041 Imobil mixt Progresului
|
||||
045 Casa Andrei Muresanu
|
||||
052 PUZ Carpenului
|
||||
059 PUZ Nordului
|
||||
064 Casa Salicea
|
||||
066 Terasa Gherase
|
||||
070 Bloc Fanatelor
|
||||
073 Case Frumoasa
|
||||
074 PUG Cosbuc
|
||||
076 Casa Copernicus
|
||||
077 PUZ Schimbare destinatie Brancusi
|
||||
078 Service auto Linistei
|
||||
079 Amenajare drum Servitute Eremia
|
||||
080 Bloc Tribunul
|
||||
081 Extindere casa Gherase
|
||||
083 Modificari casa Zsigmund 18
|
||||
084 Mansardare Petofi 21
|
||||
085 Container CT Spital Tabacarilor
|
||||
086 Imprejmuire casa sat Gheorgheni
|
||||
087 Duplex Oasului fn
|
||||
089 PUZ A-Liu Sopor
|
||||
090 VR MedEvents
|
||||
091 Reclama Caparol
|
||||
092 Imobil birouri 13 Septembrie
|
||||
093 Casa Salistea Noua
|
||||
094 PUD Casa Rediu
|
||||
095 Duplex Vanatorului
|
||||
096 Design apartament Sopor
|
||||
097 Cabana Gilau
|
||||
101 PUZ Gilau
|
||||
102 PUZ Ghimbav
|
||||
103 Piscine Lunca Noua
|
||||
104 PUZ REGHIN
|
||||
105 CUT&Crust
|
||||
106 PUZ Mihai Romanu Nord
|
||||
108 Reabilitare Bloc Beiusului
|
||||
109 Case Samboleni
|
||||
110 Penny Crasna
|
||||
111 Anexa Piscina Borhanci
|
||||
112 PUZ Blocuri Bistrita
|
||||
113 PUZ VARATEC-FIRIZA
|
||||
114 PUG Husi
|
||||
115 PUG Josenii Bargaului
|
||||
116 PUG Monor
|
||||
117 Schimbare Destinatie Mihai Viteazu 2
|
||||
120 Anexa Brasov
|
||||
121 Imprejurare imobil Mesterul Manole 9
|
||||
122 Fastfood Bashar
|
||||
123 PUD Rediu 2
|
||||
127 Casa Socaciu Ciurila
|
||||
128 Schimbare de destinatie Danubius
|
||||
129 (re) Casa Sarca-Sorescu
|
||||
130 Casa Suta-Wonderland
|
||||
131 PUD Oasului Hufi
|
||||
132 Reabilitare Camin Cultural Baciu
|
||||
133 PUG Feldru
|
||||
134 DALI Blocuri Murfatlar
|
||||
135 Case de vacanta Dianei
|
||||
136 PUG BROSTENI
|
||||
139 Casa Turda
|
||||
140 Releveu Bistrita (Morariu)
|
||||
141 PUZ Janovic Jeno
|
||||
142 Penny Borhanci
|
||||
143 Pavilion Politie Radauti
|
||||
149 Duplex Sorescu 31-33
|
||||
150 DALI SF Scoala Baciu
|
||||
151 Casa Alexandru Bohatiel 17
|
||||
152 PUZ Penny Tautii Magheraus
|
||||
153 PUG Banita
|
||||
155 PT Scoala Floresti
|
||||
156 Case Sorescu
|
||||
157 Gradi-Cresa Baciu
|
||||
158 Duplex Sorescu 21-23
|
||||
159 Amenajare Spatiu Grenke PBC
|
||||
160 Etajare Primaria Baciu
|
||||
161 Extindere Ap Baciu
|
||||
164 SD salon Aurel Vlaicu
|
||||
165 Reclama Marasti
|
||||
166 Catei Apahida
|
||||
167 Apartament Mircea Zaciu 13-15
|
||||
169 Casa PETRILA 37
|
||||
170 Cabana Campeni AB
|
||||
171 Camin Apahida
|
||||
L089 PUZ TUSA-BOJAN
|
||||
172 Design casa Iugoslaviei 18
|
||||
173 Reabilitare spitale Sighetu
|
||||
174 StudX UMFST
|
||||
176 - 2025 - ReAC Ansamblu rezi Bibescu
|
||||
|
||||
|
||||
CU
|
||||
Schita
|
||||
Avize
|
||||
PUD
|
||||
AO
|
||||
PUZ
|
||||
PUG
|
||||
DTAD
|
||||
DTAC
|
||||
PT
|
||||
Detalii de Executie
|
||||
Studii de fundamentare
|
||||
|
||||
Regulament
|
||||
Parte desenata
|
||||
Parte scrisa
|
||||
Consultanta client
|
||||
Macheta
|
||||
Consultanta receptie
|
||||
|
||||
Redactare
|
||||
Depunere
|
||||
Ridicare
|
||||
Verificare proiect
|
||||
|
||||
Vizita santier
|
||||
|
||||
Master MATDR
|
||||
@@ -1,694 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ro">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Beletage – Word XML Data Engine</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<!-- JSZip pentru arhivă ZIP -->
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js"
|
||||
integrity="sha512-FGv7V3GpCr3C6wz6Q4z8F1v8y4mZohwPqhwKiPfz0btvAvOE0tfLOgvBcFQncn1C3KW0y5fN9c7v1sQW8vGfMQ=="
|
||||
crossorigin="anonymous" referrerpolicy="no-referrer"></script>
|
||||
<style>
|
||||
:root {
|
||||
color-scheme: dark;
|
||||
}
|
||||
body {
|
||||
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||
margin: 0;
|
||||
padding: 1.5rem;
|
||||
background: #020617;
|
||||
color: #e5e7eb;
|
||||
}
|
||||
h1 {
|
||||
font-size: 1.7rem;
|
||||
margin-bottom: .25rem;
|
||||
}
|
||||
.subtitle {
|
||||
font-size: .9rem;
|
||||
color: #9ca3af;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.card {
|
||||
background: #020617;
|
||||
border-radius: 1rem;
|
||||
padding: 1.1rem 1.3rem;
|
||||
margin-bottom: 1rem;
|
||||
border: 1px solid #1e293b;
|
||||
box-shadow: 0 15px 35px rgba(0,0,0,.45);
|
||||
}
|
||||
label {
|
||||
font-size: .8rem;
|
||||
color: #9ca3af;
|
||||
display: block;
|
||||
margin-bottom: .2rem;
|
||||
}
|
||||
input, textarea, select {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
padding: .5rem .6rem;
|
||||
border-radius: .5rem;
|
||||
border: 1px solid #334155;
|
||||
background: #020617;
|
||||
color: #e5e7eb;
|
||||
font-family: inherit;
|
||||
font-size: .9rem;
|
||||
outline: none;
|
||||
}
|
||||
input:focus, textarea:focus, select:focus {
|
||||
border-color: #38bdf8;
|
||||
box-shadow: 0 0 0 1px #38bdf8;
|
||||
}
|
||||
textarea { min-height: 140px; resize: vertical; }
|
||||
.row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 1rem;
|
||||
}
|
||||
.col-3 { flex: 1 1 220px; }
|
||||
.col-6 { flex: 1 1 320px; }
|
||||
.col-9 { flex: 3 1 420px; }
|
||||
|
||||
button {
|
||||
padding: .55rem 1.1rem;
|
||||
border-radius: 999px;
|
||||
border: none;
|
||||
background: linear-gradient(135deg, #38bdf8, #6366f1);
|
||||
color: #fff;
|
||||
font-size: .9rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 12px 25px rgba(37,99,235,.4);
|
||||
}
|
||||
button:hover { filter: brightness(1.05); transform: translateY(-1px); }
|
||||
button:active { transform: translateY(0); box-shadow: 0 8px 18px rgba(37,99,235,.6); }
|
||||
|
||||
.btn-secondary {
|
||||
background: transparent;
|
||||
border: 1px solid #4b5563;
|
||||
box-shadow: none;
|
||||
}
|
||||
.btn-secondary:hover {
|
||||
background: #020617;
|
||||
box-shadow: 0 8px 20px rgba(0,0,0,.6);
|
||||
}
|
||||
.btn-small {
|
||||
font-size: .8rem;
|
||||
padding: .35rem .8rem;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.toggle {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: .4rem;
|
||||
font-size: .8rem;
|
||||
color: #cbd5f5;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
.toggle input { width: auto; }
|
||||
|
||||
.pill-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: .4rem;
|
||||
margin-bottom: .4rem;
|
||||
}
|
||||
.pill {
|
||||
padding: .25rem .7rem;
|
||||
border-radius: 999px;
|
||||
border: 1px solid #334155;
|
||||
font-size: .8rem;
|
||||
cursor: pointer;
|
||||
background: #020617;
|
||||
color: #e5e7eb;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: .35rem;
|
||||
}
|
||||
.pill.active {
|
||||
background: linear-gradient(135deg, #38bdf8, #6366f1);
|
||||
border-color: transparent;
|
||||
color: #0f172a;
|
||||
}
|
||||
.pill span.remove {
|
||||
font-size: .8rem;
|
||||
opacity: .7;
|
||||
}
|
||||
.pill span.remove:hover { opacity: 1; }
|
||||
|
||||
.small {
|
||||
font-size: .8rem;
|
||||
color: #9ca3af;
|
||||
margin-top: .25rem;
|
||||
}
|
||||
|
||||
pre {
|
||||
background: #020617;
|
||||
border-radius: .75rem;
|
||||
padding: .7rem .8rem;
|
||||
border: 1px solid #1f2937;
|
||||
overflow: auto;
|
||||
font-size: .8rem;
|
||||
max-height: 340px;
|
||||
}
|
||||
.badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: .15rem .45rem;
|
||||
border-radius: 999px;
|
||||
font-size: .7rem;
|
||||
background: rgba(148,163,184,.18);
|
||||
margin-right: .4rem;
|
||||
margin-bottom: .25rem;
|
||||
}
|
||||
@media (max-width: 768px) {
|
||||
body { padding: 1rem; }
|
||||
.card { padding: 1rem; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>Beletage – Word XML Data Engine</h1>
|
||||
<p class="subtitle">
|
||||
Generator de <strong>Custom XML Parts</strong> pentru Word, pe categorii (Beneficiar, Proiect, Suprafete, Meta etc.),
|
||||
cu mod <em>Simple</em> / <em>Advanced</em> și câmpuri derivate (Short, Upper, Initials) + POT/CUT pregătite.
|
||||
</p>
|
||||
|
||||
<!-- SETĂRI GLOBALE -->
|
||||
<div class="card">
|
||||
<div class="row">
|
||||
<div class="col-6">
|
||||
<label for="baseNs">Bază Namespace (se completează automat cu /Categorie)</label>
|
||||
<input id="baseNs" type="text" value="http://schemas.beletage.ro/contract">
|
||||
<div class="small">Ex: <code>http://schemas.beletage.ro/contract</code> → pentru categoria „Proiect” devine
|
||||
<code>http://schemas.beletage.ro/contract/Proiect</code>.
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-3">
|
||||
<label>Mod generare câmpuri</label>
|
||||
<div class="pill-row">
|
||||
<div class="pill active" id="modeSimplePill" onclick="setMode('simple')">Simple</div>
|
||||
<div class="pill" id="modeAdvancedPill" onclick="setMode('advanced')">Advanced</div>
|
||||
</div>
|
||||
<div class="small">
|
||||
<strong>Simple</strong>: doar câmpurile tale.<br>
|
||||
<strong>Advanced</strong>: + Short / Upper / Lower / Initials / First pentru fiecare câmp.
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-3">
|
||||
<label>Opțiuni extra</label>
|
||||
<div class="small" style="margin-top:.25rem;">
|
||||
<label class="toggle">
|
||||
<input type="checkbox" id="computeMetrics" checked>
|
||||
<span>Adaugă câmpuri POT / CUT în categoria Suprafete</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- CATEGORII -->
|
||||
<div class="card">
|
||||
<div class="row">
|
||||
<div class="col-3">
|
||||
<label>Categorii de date</label>
|
||||
<div id="categoryPills" class="pill-row"></div>
|
||||
<button class="btn-secondary btn-small" onclick="addCategoryPrompt()">+ Adaugă categorie</button>
|
||||
<div class="small">
|
||||
Exemple de organizare: <code>Beneficiar</code>, <code>Proiect</code>, <code>Suprafete</code>, <code>Meta</code>.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-9">
|
||||
<label>Câmpuri pentru categoria selectată</label>
|
||||
<textarea id="fieldsArea"></textarea>
|
||||
<div class="small">
|
||||
Un câmp pe linie. Poți edita lista. Butonul „Reset categorie la preset” reîncarcă valorile default pentru
|
||||
categoria curentă (dacă există).
|
||||
</div>
|
||||
<div style="margin-top:.5rem; display:flex; gap:.5rem; flex-wrap:wrap;">
|
||||
<button class="btn-secondary btn-small" onclick="resetCategoryToPreset()">Reset categorie la preset</button>
|
||||
<button class="btn-secondary btn-small" onclick="clearCategoryFields()">Curăță câmpurile</button>
|
||||
</div>
|
||||
<div class="small" id="nsRootInfo" style="margin-top:.6rem;"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- GENERARE & DOWNLOAD -->
|
||||
<div class="card">
|
||||
<div style="display:flex; flex-wrap:wrap; gap:.5rem; align-items:center; margin-bottom:.5rem;">
|
||||
<button onclick="generateAll()">Generează XML pentru toate categoriile</button>
|
||||
<button class="btn-secondary" onclick="downloadCurrentXml()">Descarcă XML categorie curentă</button>
|
||||
<button class="btn-secondary" onclick="downloadZipAll()">Descarcă ZIP cu toate XML-urile</button>
|
||||
</div>
|
||||
<div class="small">
|
||||
<span class="badge">Tip</span>
|
||||
În Word, fiecare fișier generat devine un Custom XML Part separat (ex: <code>BeneficiarData.xml</code>,
|
||||
<code>ProiectData.xml</code> etc.), perfect pentru organizarea mapping-urilor.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- PREVIEW -->
|
||||
<div class="card">
|
||||
<h3 style="margin-top:0;">Preview XML & XPaths</h3>
|
||||
<div class="small" style="margin-bottom:.4rem;">
|
||||
Selectează o categorie pentru a vedea XML-ul și XPaths-urile aferente.
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-6">
|
||||
<div class="badge">XML categorie curentă</div>
|
||||
<pre id="xmlPreview"></pre>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<div class="badge">XPaths categorie curentă</div>
|
||||
<pre id="xpathPreview"></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// --- PRESETURI CATEGORII ---
|
||||
const defaultPresets = {
|
||||
"Beneficiar": [
|
||||
"NumeClient",
|
||||
"Adresa",
|
||||
"CUI",
|
||||
"CNP",
|
||||
"Reprezentant",
|
||||
"Email",
|
||||
"Telefon"
|
||||
],
|
||||
"Proiect": [
|
||||
"TitluProiect",
|
||||
"AdresaImobil",
|
||||
"NrCadastral",
|
||||
"NrCF",
|
||||
"Localitate",
|
||||
"Judet"
|
||||
],
|
||||
"Suprafete": [
|
||||
"SuprafataTeren",
|
||||
"SuprafataConstruitaLaSol",
|
||||
"SuprafataDesfasurata",
|
||||
"SuprafataUtila"
|
||||
],
|
||||
"Meta": [
|
||||
"NrContract",
|
||||
"DataContract",
|
||||
"Responsabil",
|
||||
"VersiuneDocument",
|
||||
"DataGenerarii"
|
||||
]
|
||||
};
|
||||
|
||||
// --- STATE ---
|
||||
let categories = {}; // { Categorie: { fieldsText: "..." } }
|
||||
let currentCategory = "Beneficiar";
|
||||
let mode = "advanced"; // "simple" | "advanced"
|
||||
const xmlParts = {}; // { Categorie: xmlString }
|
||||
const xpathParts = {}; // { Categorie: xpathString }
|
||||
|
||||
// --- UTILITARE ---
|
||||
function sanitizeName(name) {
|
||||
if (!name) return null;
|
||||
let n = name.trim();
|
||||
if (!n) return null;
|
||||
n = n.replace(/\s+/g, "_").replace(/[^A-Za-z0-9_.-]/g, "");
|
||||
if (!/^[A-Za-z_]/.test(n)) n = "_" + n;
|
||||
return n;
|
||||
}
|
||||
|
||||
function initialsFromLabel(label) {
|
||||
if (!label) return "";
|
||||
return label.trim().split(/\s+/).map(s => s.charAt(0).toUpperCase() + ".").join("");
|
||||
}
|
||||
|
||||
function firstToken(label) {
|
||||
if (!label) return "";
|
||||
return label.trim().split(/\s+/)[0] || "";
|
||||
}
|
||||
|
||||
function getBaseNamespace() {
|
||||
const val = document.getElementById("baseNs").value.trim();
|
||||
return val || "http://schemas.beletage.ro/contract";
|
||||
}
|
||||
|
||||
function getCategoryNamespace(cat) {
|
||||
const base = getBaseNamespace();
|
||||
const safeCat = sanitizeName(cat) || cat;
|
||||
return base.replace(/\/+$/,"") + "/" + safeCat;
|
||||
}
|
||||
|
||||
function getCategoryRoot(cat) {
|
||||
const safeCat = sanitizeName(cat) || cat;
|
||||
return safeCat + "Data";
|
||||
}
|
||||
|
||||
// --- MOD SIMPLE/ADVANCED ---
|
||||
function setMode(m) {
|
||||
mode = m === "advanced" ? "advanced" : "simple";
|
||||
document.getElementById("modeSimplePill").classList.toggle("active", mode === "simple");
|
||||
document.getElementById("modeAdvancedPill").classList.toggle("active", mode === "advanced");
|
||||
// regenerăm previw dacă avem ceva
|
||||
generateAll(false);
|
||||
}
|
||||
|
||||
// --- CATEGORII: INIT, UI, STORAGE ---
|
||||
function initCategories() {
|
||||
// încarcă din localStorage, altfel default
|
||||
const saved = window.localStorage.getItem("beletage_xml_categories");
|
||||
if (saved) {
|
||||
try {
|
||||
const parsed = JSON.parse(saved);
|
||||
categories = parsed.categories || {};
|
||||
currentCategory = parsed.currentCategory || "Beneficiar";
|
||||
} catch(e) {
|
||||
Object.keys(defaultPresets).forEach(cat => {
|
||||
categories[cat] = { fieldsText: defaultPresets[cat].join("\n") };
|
||||
});
|
||||
currentCategory = "Beneficiar";
|
||||
}
|
||||
} else {
|
||||
Object.keys(defaultPresets).forEach(cat => {
|
||||
categories[cat] = { fieldsText: defaultPresets[cat].join("\n") };
|
||||
});
|
||||
currentCategory = "Beneficiar";
|
||||
}
|
||||
|
||||
renderCategoryPills();
|
||||
loadCategoryToUI(currentCategory);
|
||||
}
|
||||
|
||||
function persistCategories() {
|
||||
try {
|
||||
window.localStorage.setItem("beletage_xml_categories", JSON.stringify({
|
||||
categories,
|
||||
currentCategory
|
||||
}));
|
||||
} catch(e){}
|
||||
}
|
||||
|
||||
function renderCategoryPills() {
|
||||
const container = document.getElementById("categoryPills");
|
||||
container.innerHTML = "";
|
||||
Object.keys(categories).forEach(cat => {
|
||||
const pill = document.createElement("div");
|
||||
pill.className = "pill" + (cat === currentCategory ? " active" : "");
|
||||
pill.onclick = () => switchCategory(cat);
|
||||
pill.textContent = cat;
|
||||
|
||||
// nu permitem ștergerea preset-urilor de bază direct (doar la custom)
|
||||
if (!defaultPresets[cat]) {
|
||||
const remove = document.createElement("span");
|
||||
remove.className = "remove";
|
||||
remove.textContent = "×";
|
||||
remove.onclick = (ev) => {
|
||||
ev.stopPropagation();
|
||||
deleteCategory(cat);
|
||||
};
|
||||
pill.appendChild(remove);
|
||||
}
|
||||
container.appendChild(pill);
|
||||
});
|
||||
}
|
||||
|
||||
function switchCategory(cat) {
|
||||
saveCurrentCategoryFields();
|
||||
currentCategory = cat;
|
||||
renderCategoryPills();
|
||||
loadCategoryToUI(cat);
|
||||
updateNsRootInfo();
|
||||
showPreview(cat);
|
||||
persistCategories();
|
||||
}
|
||||
|
||||
function loadCategoryToUI(cat) {
|
||||
const area = document.getElementById("fieldsArea");
|
||||
area.value = categories[cat]?.fieldsText || "";
|
||||
updateNsRootInfo();
|
||||
}
|
||||
|
||||
function saveCurrentCategoryFields() {
|
||||
const area = document.getElementById("fieldsArea");
|
||||
if (!categories[currentCategory]) {
|
||||
categories[currentCategory] = { fieldsText: "" };
|
||||
}
|
||||
categories[currentCategory].fieldsText = area.value;
|
||||
}
|
||||
|
||||
function deleteCategory(cat) {
|
||||
if (!confirm(`Sigur ștergi categoria "${cat}"?`)) return;
|
||||
delete categories[cat];
|
||||
const keys = Object.keys(categories);
|
||||
currentCategory = keys[0] || "Beneficiar";
|
||||
renderCategoryPills();
|
||||
loadCategoryToUI(currentCategory);
|
||||
updateNsRootInfo();
|
||||
persistCategories();
|
||||
}
|
||||
|
||||
function addCategoryPrompt() {
|
||||
const name = prompt("Nume categorie nouă (ex: Urbanism, Fiscal, Altele):");
|
||||
if (!name) return;
|
||||
const trimmed = name.trim();
|
||||
if (!trimmed) return;
|
||||
if (categories[trimmed]) {
|
||||
alert("Categoria există deja.");
|
||||
return;
|
||||
}
|
||||
categories[trimmed] = { fieldsText: "" };
|
||||
currentCategory = trimmed;
|
||||
renderCategoryPills();
|
||||
loadCategoryToUI(currentCategory);
|
||||
updateNsRootInfo();
|
||||
persistCategories();
|
||||
}
|
||||
|
||||
function resetCategoryToPreset() {
|
||||
if (!defaultPresets[currentCategory]) {
|
||||
alert("Categoria curentă nu are preset definit.");
|
||||
return;
|
||||
}
|
||||
if (!confirm("Resetezi lista de câmpuri la presetul standard pentru această categorie?")) return;
|
||||
categories[currentCategory].fieldsText = defaultPresets[currentCategory].join("\n");
|
||||
loadCategoryToUI(currentCategory);
|
||||
persistCategories();
|
||||
}
|
||||
|
||||
function clearCategoryFields() {
|
||||
categories[currentCategory].fieldsText = "";
|
||||
loadCategoryToUI(currentCategory);
|
||||
persistCategories();
|
||||
}
|
||||
|
||||
function updateNsRootInfo() {
|
||||
const ns = getCategoryNamespace(currentCategory);
|
||||
const root = getCategoryRoot(currentCategory);
|
||||
document.getElementById("nsRootInfo").innerHTML =
|
||||
`<strong>Namespace:</strong> <code>${ns}</code><br>` +
|
||||
`<strong>Root element:</strong> <code><${root}></code>`;
|
||||
}
|
||||
|
||||
// --- GENERARE XML PENTRU O CATEGORIE ---
|
||||
function generateCategory(cat) {
|
||||
const entry = categories[cat];
|
||||
if (!entry) return { xml: "", xpaths: "" };
|
||||
|
||||
const raw = (entry.fieldsText || "").split(/\r?\n/)
|
||||
.map(l => l.trim())
|
||||
.filter(l => l.length > 0);
|
||||
|
||||
if (raw.length === 0) {
|
||||
return { xml: "", xpaths: "" };
|
||||
}
|
||||
|
||||
const ns = getCategoryNamespace(cat);
|
||||
const root = getCategoryRoot(cat);
|
||||
const computeMetrics = document.getElementById("computeMetrics").checked;
|
||||
|
||||
const usedNames = new Set();
|
||||
const fields = []; // { label, baseName, variants: [] }
|
||||
|
||||
for (const label of raw) {
|
||||
const base = sanitizeName(label);
|
||||
if (!base) continue;
|
||||
|
||||
let baseName = base;
|
||||
let idx = 2;
|
||||
while (usedNames.has(baseName)) {
|
||||
baseName = base + "_" + idx;
|
||||
idx++;
|
||||
}
|
||||
usedNames.add(baseName);
|
||||
|
||||
const variants = [baseName];
|
||||
if (mode === "advanced") {
|
||||
const advCandidates = [
|
||||
baseName + "Short",
|
||||
baseName + "Upper",
|
||||
baseName + "Lower",
|
||||
baseName + "Initials",
|
||||
baseName + "First"
|
||||
];
|
||||
for (let v of advCandidates) {
|
||||
let vn = v;
|
||||
let k = 2;
|
||||
while (usedNames.has(vn)) {
|
||||
vn = v + "_" + k;
|
||||
k++;
|
||||
}
|
||||
usedNames.add(vn);
|
||||
variants.push(vn);
|
||||
}
|
||||
}
|
||||
|
||||
fields.push({ label, baseName, variants });
|
||||
}
|
||||
|
||||
// detectăm câmpuri pentru metrici (în special categoria Suprafete)
|
||||
const extraMetricFields = [];
|
||||
if (computeMetrics && cat.toLowerCase().includes("suprafete")) {
|
||||
const hasTeren = fields.some(f => f.baseName.toLowerCase().includes("suprafatateren"));
|
||||
const hasLaSol = fields.some(f => f.baseName.toLowerCase().includes("suprafataconstruitalasol"));
|
||||
const hasDesf = fields.some(f => f.baseName.toLowerCase().includes("suprafatadesfasurata"));
|
||||
|
||||
if (hasTeren && hasLaSol) {
|
||||
if (!usedNames.has("POT")) {
|
||||
usedNames.add("POT");
|
||||
extraMetricFields.push({ label: "Procent Ocupare Teren", baseName: "POT", variants: ["POT"] });
|
||||
}
|
||||
}
|
||||
if (hasTeren && hasDesf) {
|
||||
if (!usedNames.has("CUT")) {
|
||||
usedNames.add("CUT");
|
||||
extraMetricFields.push({ label: "Coeficient Utilizare Teren", baseName: "CUT", variants: ["CUT"] });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// generăm XML
|
||||
let xml = '<?xml version="1.0" encoding="UTF-8"?>\n';
|
||||
xml += `<${root} xmlns="${ns}">\n`;
|
||||
|
||||
const allFieldEntries = fields.concat(extraMetricFields);
|
||||
|
||||
for (const f of allFieldEntries) {
|
||||
for (const v of f.variants) {
|
||||
xml += ` <${v}></${v}>\n`;
|
||||
}
|
||||
}
|
||||
|
||||
xml += `</${root}>\n`;
|
||||
|
||||
// generăm XPaths
|
||||
let xp = `Categorie: ${cat}\nNamespace: ${ns}\nRoot: /${root}\n\n`;
|
||||
for (const f of fields) {
|
||||
xp += `# ${f.label}\n`;
|
||||
for (const v of f.variants) {
|
||||
xp += `/${root}/${v}\n`;
|
||||
}
|
||||
xp += `\n`;
|
||||
}
|
||||
if (extraMetricFields.length > 0) {
|
||||
xp += `# Metrici auto (POT / CUT)\n`;
|
||||
for (const f of extraMetricFields) {
|
||||
for (const v of f.variants) {
|
||||
xp += `/${root}/${v}\n`;
|
||||
}
|
||||
}
|
||||
xp += `\n`;
|
||||
}
|
||||
|
||||
return { xml, xpaths: xp };
|
||||
}
|
||||
|
||||
// --- GENERARE PENTRU TOATE CATEGORIILE ---
|
||||
function generateAll(showForCurrent = true) {
|
||||
saveCurrentCategoryFields();
|
||||
Object.keys(categories).forEach(cat => {
|
||||
const { xml, xpaths } = generateCategory(cat);
|
||||
xmlParts[cat] = xml;
|
||||
xpathParts[cat] = xpaths;
|
||||
});
|
||||
if (showForCurrent) {
|
||||
showPreview(currentCategory);
|
||||
}
|
||||
persistCategories();
|
||||
}
|
||||
|
||||
// --- PREVIEW ---
|
||||
function showPreview(cat) {
|
||||
document.getElementById("xmlPreview").textContent = xmlParts[cat] || "<!-- Niciun XML generat încă pentru această categorie. -->";
|
||||
document.getElementById("xpathPreview").textContent = xpathParts[cat] || "";
|
||||
}
|
||||
|
||||
// --- DOWNLOAD: XML CATEGORIE ---
|
||||
function downloadCurrentXml() {
|
||||
generateAll(false);
|
||||
const xml = xmlParts[currentCategory];
|
||||
if (!xml) {
|
||||
alert("Nu există XML generat pentru categoria curentă. Apasă întâi „Generează XML pentru toate categoriile”.");
|
||||
return;
|
||||
}
|
||||
const root = getCategoryRoot(currentCategory);
|
||||
const fileName = root + ".xml";
|
||||
const blob = new Blob([xml], { type: "application/xml" });
|
||||
const a = document.createElement("a");
|
||||
a.href = URL.createObjectURL(blob);
|
||||
a.download = fileName;
|
||||
a.click();
|
||||
URL.revokeObjectURL(a.href);
|
||||
}
|
||||
|
||||
// --- DOWNLOAD: ZIP CU TOATE XML-URILE ---
|
||||
async function downloadZipAll() {
|
||||
generateAll(false);
|
||||
const cats = Object.keys(categories);
|
||||
if (cats.length === 0) {
|
||||
alert("Nu există categorii.");
|
||||
return;
|
||||
}
|
||||
|
||||
const zip = new JSZip();
|
||||
const folder = zip.folder("customXmlParts");
|
||||
|
||||
let hasAny = false;
|
||||
for (const cat of cats) {
|
||||
const xml = xmlParts[cat];
|
||||
if (!xml) continue;
|
||||
hasAny = true;
|
||||
const root = getCategoryRoot(cat);
|
||||
const fileName = root + ".xml";
|
||||
folder.file(fileName, xml);
|
||||
}
|
||||
|
||||
if (!hasAny) {
|
||||
alert("Nu există XML generat încă. Apasă întâi „Generează XML pentru toate categoriile”.");
|
||||
return;
|
||||
}
|
||||
|
||||
const content = await zip.generateAsync({ type: "blob" });
|
||||
const a = document.createElement("a");
|
||||
a.href = URL.createObjectURL(content);
|
||||
a.download = "beletage_custom_xml_parts.zip";
|
||||
a.click();
|
||||
URL.revokeObjectURL(a.href);
|
||||
}
|
||||
|
||||
// --- INIT ---
|
||||
window.addEventListener("DOMContentLoaded", () => {
|
||||
initCategories();
|
||||
updateNsRootInfo();
|
||||
generateAll();
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,151 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ro">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Generator XML Word – Versiune Extinsă</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<style>
|
||||
body {
|
||||
font-family: system-ui, sans-serif;
|
||||
padding: 1.5rem;
|
||||
background: #0f172a;
|
||||
color: #e5e7eb;
|
||||
}
|
||||
.card {
|
||||
background: #020617;
|
||||
border-radius: 1rem;
|
||||
padding: 1.25rem;
|
||||
box-shadow: 0 10px 30px rgba(0,0,0,0.3);
|
||||
border: 1px solid #1e293b;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
label { font-size: .85rem; color: #94a3b8; }
|
||||
input, textarea {
|
||||
width: 100%; padding: .55rem .7rem;
|
||||
border-radius: .5rem; border: 1px solid #334155;
|
||||
background: #020617; color: #e5e7eb;
|
||||
}
|
||||
textarea { min-height: 120px; }
|
||||
button {
|
||||
padding: .6rem 1.2rem; border-radius: 999px; border: none;
|
||||
background: linear-gradient(135deg,#38bdf8,#6366f1);
|
||||
font-weight: 600; color: white; cursor: pointer;
|
||||
}
|
||||
pre {
|
||||
background: #000; padding: .8rem; border-radius: .7rem;
|
||||
border: 1px solid #1e293b; max-height: 350px; overflow: auto;
|
||||
font-size: .85rem;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<h1>Generator Word XML – Varianta Extinsă (cu Short / Upper / Lower / Initials)</h1>
|
||||
|
||||
<div class="card">
|
||||
<label>Namespace URI</label>
|
||||
<input id="nsUri" value="http://schemas.beletage.ro/word/contract">
|
||||
|
||||
<label style="margin-top:1rem;">Element rădăcină</label>
|
||||
<input id="rootElement" value="ContractData">
|
||||
|
||||
<label style="margin-top:1rem;">Lista de câmpuri (unul pe linie)</label>
|
||||
<textarea id="fieldList">NumeClient
|
||||
TitluProiect
|
||||
Adresa</textarea>
|
||||
|
||||
<button onclick="generateXML()" style="margin-top:1rem;">Generează XML complet</button>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h3>Custom XML Part (item1.xml)</h3>
|
||||
<pre id="xmlOutput"></pre>
|
||||
<button onclick="downloadXML()">Descarcă XML</button>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h3>XPaths pentru mapping</h3>
|
||||
<pre id="xpathOutput"></pre>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function sanitize(name) {
|
||||
if (!name) return null;
|
||||
let n = name.trim();
|
||||
if (!n) return null;
|
||||
n = n.replace(/\s+/g,"_").replace(/[^A-Za-z0-9_.-]/g,"");
|
||||
if (!/^[A-Za-z_]/.test(n)) n = "_" + n;
|
||||
return n;
|
||||
}
|
||||
|
||||
function initials(str) {
|
||||
return str.split(/\s+/).map(s => s[0]?.toUpperCase() + ".").join("");
|
||||
}
|
||||
|
||||
function generateXML() {
|
||||
const ns = document.getElementById("nsUri").value.trim();
|
||||
const root = sanitize(document.getElementById("rootElement").value) || "Root";
|
||||
const fieldRaw = document.getElementById("fieldList").value;
|
||||
|
||||
const lines = fieldRaw.split(/\r?\n/)
|
||||
.map(l => l.trim()).filter(l => l.length);
|
||||
|
||||
const fields = [];
|
||||
|
||||
for (let l of lines) {
|
||||
const base = sanitize(l);
|
||||
if (!base) continue;
|
||||
|
||||
fields.push({
|
||||
base,
|
||||
variants: [
|
||||
base, // original
|
||||
base + "Short", // prescurtat
|
||||
base + "Upper", // caps
|
||||
base + "Lower", // lowercase
|
||||
base + "Initials", // inițiale
|
||||
base + "First" // primul cuvânt
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
// === GENERĂM XML ===
|
||||
let xml = '<?xml version="1.0" encoding="UTF-8"?>\n';
|
||||
xml += `<${root} xmlns="${ns}">\n`;
|
||||
|
||||
for (const f of fields) {
|
||||
for (const v of f.variants) {
|
||||
xml += ` <${v}></${v}>\n`;
|
||||
}
|
||||
}
|
||||
|
||||
xml += `</${root}>`;
|
||||
|
||||
document.getElementById("xmlOutput").textContent = xml;
|
||||
|
||||
// === GENERĂM XPATHS ===
|
||||
let xp = `Namespace: ${ns}\nRoot: /${root}\n\n`;
|
||||
for (const f of fields) {
|
||||
xp += `# ${f.base}\n`;
|
||||
xp += `/${root}/${f.base}\n`;
|
||||
xp += `/${root}/${f.base}Short\n`;
|
||||
xp += `/${root}/${f.base}Upper\n`;
|
||||
xp += `/${root}/${f.base}Lower\n`;
|
||||
xp += `/${root}/${f.base}Initials\n`;
|
||||
xp += `/${root}/${f.base}First\n\n`;
|
||||
}
|
||||
document.getElementById("xpathOutput").textContent = xp;
|
||||
}
|
||||
|
||||
function downloadXML() {
|
||||
const text = document.getElementById("xmlOutput").textContent;
|
||||
const blob = new Blob([text], { type: "application/xml" });
|
||||
const a = document.createElement("a");
|
||||
a.href = URL.createObjectURL(blob);
|
||||
a.download = "item1.xml";
|
||||
a.click();
|
||||
}
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,330 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ro">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Generator Word XML Custom Part</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<style>
|
||||
body {
|
||||
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||
margin: 0;
|
||||
padding: 1.5rem;
|
||||
background: #0f172a;
|
||||
color: #e5e7eb;
|
||||
}
|
||||
h1 {
|
||||
font-size: 1.6rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
.container {
|
||||
max-width: 1100px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.card {
|
||||
background: #020617;
|
||||
border-radius: 1rem;
|
||||
padding: 1.25rem 1.5rem;
|
||||
box-shadow: 0 15px 40px rgba(0,0,0,0.35);
|
||||
border: 1px solid #1f2937;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
label {
|
||||
display: block;
|
||||
font-size: 0.85rem;
|
||||
color: #9ca3af;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
input, textarea {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
padding: 0.5rem 0.6rem;
|
||||
border-radius: 0.5rem;
|
||||
border: 1px solid #374151;
|
||||
background: #020617;
|
||||
color: #e5e7eb;
|
||||
font-family: inherit;
|
||||
font-size: 0.9rem;
|
||||
outline: none;
|
||||
}
|
||||
input:focus, textarea:focus {
|
||||
border-color: #38bdf8;
|
||||
box-shadow: 0 0 0 1px #38bdf8;
|
||||
}
|
||||
textarea {
|
||||
min-height: 140px;
|
||||
resize: vertical;
|
||||
}
|
||||
.row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 1rem;
|
||||
}
|
||||
.col-6 {
|
||||
flex: 1 1 260px;
|
||||
}
|
||||
button {
|
||||
padding: 0.6rem 1.2rem;
|
||||
border-radius: 999px;
|
||||
border: none;
|
||||
font-size: 0.9rem;
|
||||
cursor: pointer;
|
||||
margin-top: 0.75rem;
|
||||
background: linear-gradient(135deg, #38bdf8, #6366f1);
|
||||
color: white;
|
||||
font-weight: 600;
|
||||
box-shadow: 0 10px 25px rgba(37, 99, 235, 0.4);
|
||||
}
|
||||
button:hover {
|
||||
filter: brightness(1.05);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
button:active {
|
||||
transform: translateY(0);
|
||||
box-shadow: 0 6px 18px rgba(37,99,235,0.6);
|
||||
}
|
||||
pre {
|
||||
background: #020617;
|
||||
border-radius: 0.75rem;
|
||||
padding: 0.75rem 1rem;
|
||||
overflow: auto;
|
||||
font-size: 0.8rem;
|
||||
border: 1px solid #1f2937;
|
||||
max-height: 360px;
|
||||
}
|
||||
.subtitle {
|
||||
font-size: 0.85rem;
|
||||
color: #9ca3af;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.pill {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
font-size: 0.75rem;
|
||||
padding: 0.2rem 0.5rem;
|
||||
border-radius: 999px;
|
||||
background: rgba(148, 163, 184, 0.2);
|
||||
margin-right: 0.25rem;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
.pill span {
|
||||
opacity: 0.8;
|
||||
}
|
||||
.small {
|
||||
font-size: 0.8rem;
|
||||
color: #9ca3af;
|
||||
margin-top: 0.4rem;
|
||||
}
|
||||
.btn-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
margin-top: 0.75rem;
|
||||
}
|
||||
.btn-secondary {
|
||||
background: transparent;
|
||||
border: 1px solid #4b5563;
|
||||
box-shadow: none;
|
||||
color: #e5e7eb;
|
||||
}
|
||||
.btn-secondary:hover {
|
||||
background: #111827;
|
||||
box-shadow: 0 8px 18px rgba(0,0,0,0.5);
|
||||
}
|
||||
@media (max-width: 640px) {
|
||||
body {
|
||||
padding: 1rem;
|
||||
}
|
||||
.card {
|
||||
padding: 1rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>Generator XML pentru Word Custom XML Part</h1>
|
||||
<p class="subtitle">
|
||||
Introdu câmpurile (unul pe linie) și obții XML pentru <strong>Custom XML Part</strong>, plus XPaths pentru mapping în Word.
|
||||
</p>
|
||||
|
||||
<div class="card">
|
||||
<div class="row">
|
||||
<div class="col-6">
|
||||
<label for="nsUri">Namespace URI (obligatoriu)</label>
|
||||
<input id="nsUri" type="text"
|
||||
value="http://schemas.beletage.ro/word/data">
|
||||
<div class="small">
|
||||
Exemplu: <code>http://schemas.firma-ta.ro/word/contract</code>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<label for="rootElement">Nume element rădăcină</label>
|
||||
<input id="rootElement" type="text" value="Root">
|
||||
<div class="small">
|
||||
Exemplu: <code>ContractData</code>, <code>ClientInfo</code> etc.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="margin-top:1rem;">
|
||||
<label for="fieldList">Lista de câmpuri (unul pe linie)</label>
|
||||
<textarea id="fieldList" placeholder="Exemplu:
|
||||
NumeClient
|
||||
Adresa
|
||||
DataContract
|
||||
ValoareTotala"></textarea>
|
||||
<div class="small">
|
||||
Numele va fi curățat automat pentru a fi valid ca nume de element XML
|
||||
(spațiile devin <code>_</code>, caracterele ciudate se elimină).
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="btn-row">
|
||||
<button type="button" onclick="generateXML()">Generează XML</button>
|
||||
<button type="button" class="btn-secondary" onclick="fillDemo()">Exemplu demo</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="pill"><strong>1</strong><span>Custom XML Part (item1.xml)</span></div>
|
||||
<pre id="xmlOutput"></pre>
|
||||
<div class="btn-row">
|
||||
<button type="button" class="btn-secondary" onclick="copyToClipboard('xmlOutput')">
|
||||
Copiază XML
|
||||
</button>
|
||||
<button type="button" class="btn-secondary" onclick="downloadXML()">
|
||||
Descarcă item1.xml
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="pill"><strong>2</strong><span>XPaths pentru mapping în Word</span></div>
|
||||
<pre id="xpathOutput"></pre>
|
||||
<button type="button" class="btn-secondary" onclick="copyToClipboard('xpathOutput')">
|
||||
Copiază XPaths
|
||||
</button>
|
||||
<p class="small">
|
||||
În Word → <strong>Developer</strong> → <strong>XML Mapping Pane</strong> → alegi Custom XML Part-ul
|
||||
→ pentru fiecare câmp, click dreapta → <em>Insert Content Control</em> → tipul dorit.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function sanitizeXmlName(name) {
|
||||
if (!name) return null;
|
||||
let n = name.trim();
|
||||
if (!n) return null;
|
||||
|
||||
// înlocuim spații cu underscore
|
||||
n = n.replace(/\s+/g, "_");
|
||||
|
||||
// eliminăm caractere invalide pentru nume de element XML
|
||||
n = n.replace(/[^A-Za-z0-9_.-]/g, "");
|
||||
|
||||
// numele XML nu are voie să înceapă cu cifră sau punct sau cratimă
|
||||
if (!/^[A-Za-z_]/.test(n)) {
|
||||
n = "_" + n;
|
||||
}
|
||||
|
||||
return n || null;
|
||||
}
|
||||
|
||||
function generateXML() {
|
||||
const nsUri = document.getElementById("nsUri").value.trim();
|
||||
const root = sanitizeXmlName(document.getElementById("rootElement").value) || "Root";
|
||||
const fieldRaw = document.getElementById("fieldList").value;
|
||||
const xmlOutput = document.getElementById("xmlOutput");
|
||||
const xpathOutput = document.getElementById("xpathOutput");
|
||||
|
||||
if (!nsUri) {
|
||||
alert("Te rog completează Namespace URI.");
|
||||
return;
|
||||
}
|
||||
|
||||
const lines = fieldRaw.split(/\r?\n/)
|
||||
.map(l => l.trim())
|
||||
.filter(l => l.length > 0);
|
||||
|
||||
const fields = [];
|
||||
const used = new Set();
|
||||
|
||||
for (let line of lines) {
|
||||
const clean = sanitizeXmlName(line);
|
||||
if (!clean) continue;
|
||||
let finalName = clean;
|
||||
let idx = 2;
|
||||
while (used.has(finalName)) {
|
||||
finalName = clean + "_" + idx;
|
||||
idx++;
|
||||
}
|
||||
used.add(finalName);
|
||||
fields.push({ original: line, xmlName: finalName });
|
||||
}
|
||||
|
||||
if (fields.length === 0) {
|
||||
xmlOutput.textContent = "<!-- Niciun câmp valid. Completează lista de câmpuri. -->";
|
||||
xpathOutput.textContent = "";
|
||||
return;
|
||||
}
|
||||
|
||||
// Generăm XML-ul pentru Custom XML Part
|
||||
let xml = '<?xml version="1.0" encoding="UTF-8"?>\n';
|
||||
xml += `<${root} xmlns="${nsUri}">\n`;
|
||||
for (const f of fields) {
|
||||
xml += ` <${f.xmlName}></${f.xmlName}>\n`;
|
||||
}
|
||||
xml += `</${root}>\n`;
|
||||
|
||||
xmlOutput.textContent = xml;
|
||||
|
||||
// Generăm lista de XPaths
|
||||
let xpaths = `Namespace: ${nsUri}\nRoot: /${root}\n\nCâmpuri:\n`;
|
||||
for (const f of fields) {
|
||||
xpaths += `- ${f.original} => /${root}/${f.xmlName}\n`;
|
||||
}
|
||||
xpathOutput.textContent = xpaths;
|
||||
}
|
||||
|
||||
function copyToClipboard(elementId) {
|
||||
const el = document.getElementById(elementId);
|
||||
if (!el || !el.textContent) return;
|
||||
navigator.clipboard.writeText(el.textContent)
|
||||
.then(() => alert("Copiat în clipboard."))
|
||||
.catch(() => alert("Nu am reușit să copiez în clipboard."));
|
||||
}
|
||||
|
||||
function downloadXML() {
|
||||
const xmlText = document.getElementById("xmlOutput").textContent;
|
||||
if (!xmlText || xmlText.startsWith("<!--")) {
|
||||
alert("Nu există XML valid de descărcat.");
|
||||
return;
|
||||
}
|
||||
const blob = new Blob([xmlText], { type: "application/xml" });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
a.download = "item1.xml";
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
function fillDemo() {
|
||||
document.getElementById("nsUri").value = "http://schemas.beletage.ro/word/contract";
|
||||
document.getElementById("rootElement").value = "ContractData";
|
||||
document.getElementById("fieldList").value = [
|
||||
"NumeClient",
|
||||
"AdresaClient",
|
||||
"Proiect",
|
||||
"DataContract",
|
||||
"ValoareTotala",
|
||||
"Moneda",
|
||||
"TermenExecutie"
|
||||
].join("\n");
|
||||
generateXML();
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -6,6 +6,19 @@ const nextConfig: NextConfig = {
|
||||
experimental: {
|
||||
middlewareClientMaxBodySize: '500mb',
|
||||
},
|
||||
async headers() {
|
||||
return [
|
||||
{
|
||||
source: '/(.*)',
|
||||
headers: [
|
||||
{ key: 'X-Frame-Options', value: 'SAMEORIGIN' },
|
||||
{ key: 'X-Content-Type-Options', value: 'nosniff' },
|
||||
{ key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin' },
|
||||
{ key: 'Permissions-Policy', value: 'camera=(), microphone=(), geolocation=()' },
|
||||
],
|
||||
},
|
||||
];
|
||||
},
|
||||
async rewrites() {
|
||||
const martinUrl = process.env.MARTIN_URL || 'http://martin:3000';
|
||||
return [
|
||||
|
||||
@@ -42,7 +42,7 @@ model GisFeature {
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
syncRun GisSyncRun? @relation(fields: [syncRunId], references: [id])
|
||||
syncRun GisSyncRun? @relation(fields: [syncRunId], references: [id], onDelete: SetNull)
|
||||
|
||||
@@unique([layerId, objectId])
|
||||
@@index([siruta])
|
||||
|
||||
@@ -0,0 +1,878 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useCallback, useEffect } from "react";
|
||||
import { Button } from "@/shared/components/ui/button";
|
||||
import { Badge } from "@/shared/components/ui/badge";
|
||||
import { Card, CardContent } from "@/shared/components/ui/card";
|
||||
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/shared/components/ui/tabs";
|
||||
import {
|
||||
Copy,
|
||||
Check,
|
||||
ChevronDown,
|
||||
ChevronRight,
|
||||
Terminal,
|
||||
Bug,
|
||||
Sparkles,
|
||||
Shield,
|
||||
TestTube,
|
||||
Plug,
|
||||
RefreshCw,
|
||||
FileText,
|
||||
Zap,
|
||||
Rocket,
|
||||
ListChecks,
|
||||
Brain,
|
||||
Wrench,
|
||||
} from "lucide-react";
|
||||
|
||||
// ─── Types ─────────────────────────────────────────────────────────
|
||||
|
||||
type PromptCategory = {
|
||||
id: string;
|
||||
label: string;
|
||||
icon: React.ReactNode;
|
||||
description: string;
|
||||
prompts: PromptTemplate[];
|
||||
};
|
||||
|
||||
type PromptTemplate = {
|
||||
id: string;
|
||||
title: string;
|
||||
description: string;
|
||||
prompt: string;
|
||||
tags: string[];
|
||||
oneTime?: boolean;
|
||||
variables?: string[];
|
||||
};
|
||||
|
||||
// ─── Module list for variable substitution hints ───────────────────
|
||||
|
||||
const MODULES = [
|
||||
"registratura", "address-book", "parcel-sync", "geoportal", "password-vault",
|
||||
"mini-utilities", "email-signature", "word-xml", "word-templates", "tag-manager",
|
||||
"it-inventory", "digital-signatures", "prompt-generator", "ai-chat", "hot-desk",
|
||||
"visual-copilot", "dashboard",
|
||||
] as const;
|
||||
|
||||
// ─── Prompt Templates ──────────────────────────────────────────────
|
||||
|
||||
const CATEGORIES: PromptCategory[] = [
|
||||
{
|
||||
id: "module-work",
|
||||
label: "Lucru pe modul",
|
||||
icon: <Terminal className="size-4" />,
|
||||
description: "Prompturi pentru lucru general, bugfix-uri si features pe module existente",
|
||||
prompts: [
|
||||
{
|
||||
id: "module-work-general",
|
||||
title: "Lucru general pe modul",
|
||||
description: "Sesiune de lucru pe un modul specific — citeste contextul, propune, implementeaza",
|
||||
tags: ["regular", "module"],
|
||||
variables: ["MODULE_NAME"],
|
||||
prompt: `Scopul acestei sesiuni este sa lucram pe modulul {MODULE_NAME} din ArchiTools.
|
||||
|
||||
Citeste CLAUDE.md si docs/MODULE-MAP.md inainte de orice.
|
||||
Apoi citeste tipurile si componentele modulului:
|
||||
- src/modules/{MODULE_NAME}/types.ts
|
||||
- src/modules/{MODULE_NAME}/config.ts
|
||||
- src/modules/{MODULE_NAME}/components/ (fisierele principale)
|
||||
- src/modules/{MODULE_NAME}/services/ (daca exista)
|
||||
|
||||
Dupa ce ai inteles codul existent, intreaba-ma ce vreau sa fac.
|
||||
Nu propune schimbari pana nu intelegi modulul complet.
|
||||
npx next build TREBUIE sa treaca dupa fiecare schimbare.`,
|
||||
},
|
||||
{
|
||||
id: "bugfix",
|
||||
title: "Bugfix pe modul",
|
||||
description: "Investigheaza si rezolva un bug specific",
|
||||
tags: ["regular", "bugfix"],
|
||||
variables: ["MODULE_NAME", "BUG_DESCRIPTION"],
|
||||
prompt: `Am un bug in modulul {MODULE_NAME}: {BUG_DESCRIPTION}
|
||||
|
||||
Citeste CLAUDE.md si docs/MODULE-MAP.md pentru context.
|
||||
|
||||
Pasi:
|
||||
1. Citeste fisierele relevante ale modulului
|
||||
2. Identifica cauza root — nu ghici, citeste codul
|
||||
3. Propune fix-ul INAINTE sa-l aplici
|
||||
4. Aplica fix-ul minimal (nu refactoriza alte lucruri)
|
||||
5. Verifica ca npx next build trece
|
||||
6. Explica ce s-a schimbat si de ce
|
||||
|
||||
Daca bug-ul e in interactiunea cu alt modul sau API, citeste si acel cod.
|
||||
Nu adauga features sau "imbunatatiri" — doar fix bug-ul raportat.`,
|
||||
},
|
||||
{
|
||||
id: "feature-existing",
|
||||
title: "Feature nou in modul existent",
|
||||
description: "Adauga o functionalitate noua intr-un modul care exista deja",
|
||||
tags: ["regular", "feature"],
|
||||
variables: ["MODULE_NAME", "FEATURE_DESCRIPTION"],
|
||||
prompt: `Vreau sa adaug un feature in modulul {MODULE_NAME}: {FEATURE_DESCRIPTION}
|
||||
|
||||
Citeste CLAUDE.md si docs/MODULE-MAP.md pentru context.
|
||||
|
||||
Pasi obligatorii:
|
||||
1. Citeste types.ts, config.ts si componentele existente ale modulului
|
||||
2. Verifica daca feature-ul necesita schimbari de tip (types.ts)
|
||||
3. Verifica daca necesita API route nou sau modificare la cel existent
|
||||
4. Propune planul de implementare INAINTE de a scrie cod
|
||||
5. Implementeaza pas cu pas, verificand build dupa fiecare fisier major
|
||||
6. Pastreaza conventiile existente (English code, Romanian UI)
|
||||
7. npx next build TREBUIE sa treaca
|
||||
|
||||
Reguli:
|
||||
- Nu schimba structura modulului fara motiv
|
||||
- Nu adauga dependinte noi daca nu e necesar
|
||||
- Pastreaza compatibilitatea cu datele existente in storage/DB
|
||||
- Daca trebuie migrare de date, propune-o separat`,
|
||||
},
|
||||
{
|
||||
id: "feature-new-module",
|
||||
title: "Modul complet nou",
|
||||
description: "Creeaza un modul nou de la zero urmand pattern-ul standard",
|
||||
tags: ["one-time", "feature", "architecture"],
|
||||
oneTime: true,
|
||||
variables: ["MODULE_NAME", "MODULE_DESCRIPTION"],
|
||||
prompt: `Vreau sa creez un modul nou: {MODULE_NAME} — {MODULE_DESCRIPTION}
|
||||
|
||||
Citeste CLAUDE.md, docs/MODULE-MAP.md si docs/guides/MODULE-DEVELOPMENT.md.
|
||||
Studiaza un modul existent similar ca referinta (ex: it-inventory sau hot-desk pentru module simple, registratura pentru module complexe).
|
||||
|
||||
Creeaza structura standard:
|
||||
src/modules/{MODULE_NAME}/
|
||||
components/{MODULE_NAME}-module.tsx
|
||||
hooks/use-{MODULE_NAME}.ts (daca e nevoie)
|
||||
services/ (daca e nevoie)
|
||||
types.ts
|
||||
config.ts
|
||||
index.ts
|
||||
|
||||
Plus:
|
||||
- src/app/(modules)/{MODULE_NAME}/page.tsx (route page)
|
||||
- Adauga config in src/config/modules.ts
|
||||
- Adauga flag in src/config/flags.ts
|
||||
- Adauga navigare in src/config/navigation.ts
|
||||
|
||||
Reguli:
|
||||
- Urmeaza EXACT pattern-ul celorlalte module
|
||||
- English code, Romanian UI text
|
||||
- Feature flag enabled by default
|
||||
- Storage via useStorage('{MODULE_NAME}') hook
|
||||
- npx next build TREBUIE sa treaca
|
||||
- Nu implementa mai mult decat MVP-ul — pot adauga dupa`,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "api",
|
||||
label: "API & Backend",
|
||||
icon: <Plug className="size-4" />,
|
||||
description: "Creare si modificare API routes, integrari externe, Prisma schema",
|
||||
prompts: [
|
||||
{
|
||||
id: "api-new-route",
|
||||
title: "API route nou",
|
||||
description: "Creeaza un endpoint API nou cu auth, validare, error handling",
|
||||
tags: ["regular", "api"],
|
||||
variables: ["ROUTE_PATH", "ROUTE_DESCRIPTION"],
|
||||
prompt: `Creeaza un API route nou: /api/{ROUTE_PATH} — {ROUTE_DESCRIPTION}
|
||||
|
||||
Citeste CLAUDE.md (sectiunea Middleware & Large Uploads) si docs/ARCHITECTURE-QUICK.md.
|
||||
|
||||
Cerinte obligatorii:
|
||||
1. Auth: middleware coverage SAU requireAuth() pentru rute excluse
|
||||
2. Input validation pe toate parametrii
|
||||
3. Error handling: try/catch cu mesaje utile (nu stack traces)
|
||||
4. Prisma queries: parametrizate ($queryRaw cu template literals, NU string concat)
|
||||
5. TypeScript strict: toate return types explicit
|
||||
|
||||
Pattern de referinta — citeste un API route existent similar:
|
||||
- CRUD simplu: src/app/api/storage/route.ts
|
||||
- Cu Prisma raw: src/app/api/registratura/route.ts
|
||||
- Cu external API: src/app/api/eterra/search/route.ts
|
||||
|
||||
Daca ruta accepta uploads mari:
|
||||
- Exclude din middleware matcher (src/middleware.ts)
|
||||
- Adauga requireAuth() manual
|
||||
- Documenteaza in CLAUDE.md sectiunea Middleware
|
||||
|
||||
npx next build TREBUIE sa treaca.`,
|
||||
},
|
||||
{
|
||||
id: "prisma-schema",
|
||||
title: "Modificare Prisma schema",
|
||||
description: "Adauga model nou sau modifica schema existenta",
|
||||
tags: ["regular", "database"],
|
||||
variables: ["CHANGE_DESCRIPTION"],
|
||||
prompt: `Vreau sa modific Prisma schema: {CHANGE_DESCRIPTION}
|
||||
|
||||
Citeste prisma/schema.prisma complet inainte.
|
||||
|
||||
Pasi:
|
||||
1. Propune schimbarea de schema INAINTE de a o aplica
|
||||
2. Verifica impactul asupra codului existent (grep pentru modelul afectat)
|
||||
3. Aplica in schema.prisma
|
||||
4. Ruleaza: npx prisma generate
|
||||
5. Actualizeaza codul care foloseste modelul
|
||||
6. npx next build TREBUIE sa treaca
|
||||
|
||||
Reguli:
|
||||
- Adauga @@index pe coloane folosite in WHERE/ORDER BY
|
||||
- Adauga @@unique pe combinatii care trebuie sa fie unice
|
||||
- onDelete: SetNull sau Cascade — niciodata default (restrict)
|
||||
- Foloseste Json? pentru campuri flexibile (enrichment pattern)
|
||||
- DateTime cu @default(now()) pe createdAt, @updatedAt pe updatedAt
|
||||
|
||||
IMPORTANT: Aceasta schimbare necesita si migrare pe server (prisma migrate).
|
||||
Nu face breaking changes fara plan de migrare.`,
|
||||
},
|
||||
{
|
||||
id: "external-integration",
|
||||
title: "Integrare API extern",
|
||||
description: "Conectare la un serviciu extern (pattern eTerra/ePay)",
|
||||
tags: ["one-time", "api", "architecture"],
|
||||
oneTime: true,
|
||||
variables: ["SERVICE_NAME", "SERVICE_DESCRIPTION"],
|
||||
prompt: `Vreau sa integrez un serviciu extern: {SERVICE_NAME} — {SERVICE_DESCRIPTION}
|
||||
|
||||
Citeste CLAUDE.md sectiunile eTerra/ANCPI Rules si Middleware.
|
||||
Studiaza pattern-ul din src/modules/parcel-sync/services/eterra-client.ts ca referinta.
|
||||
|
||||
Pattern obligatoriu pentru integrari externe:
|
||||
1. Client class separat in services/ (nu inline in route)
|
||||
2. Session/token caching cu TTL (global singleton pattern)
|
||||
3. Periodic cleanup pe cache (setInterval)
|
||||
4. Health check daca serviciul e instabil
|
||||
5. Retry logic pentru erori tranziente (ECONNRESET, 500)
|
||||
6. Timeout explicit pe toate request-urile
|
||||
7. Error handling granular (nu catch-all generic)
|
||||
8. Logging cu prefix: console.log("[{SERVICE_NAME}] ...")
|
||||
|
||||
Env vars:
|
||||
- Adauga in docker-compose.yml
|
||||
- NU hardcoda credentials in cod
|
||||
- Documenteaza in docs/ARCHITECTURE-QUICK.md
|
||||
|
||||
npx next build TREBUIE sa treaca.`,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "quality",
|
||||
label: "Calitate & Securitate",
|
||||
icon: <Shield className="size-4" />,
|
||||
description: "Audituri de securitate, testing, performance, code review",
|
||||
prompts: [
|
||||
{
|
||||
id: "security-audit",
|
||||
title: "Audit securitate complet",
|
||||
description: "Scanare completa de securitate pe tot codebase-ul",
|
||||
tags: ["periodic", "security"],
|
||||
prompt: `Scopul acestei sesiuni este un audit complet de securitate al ArchiTools.
|
||||
Aplicatia este IN PRODUCTIE la https://tools.beletage.ro.
|
||||
|
||||
Citeste CLAUDE.md si docs/ARCHITECTURE-QUICK.md inainte de orice.
|
||||
|
||||
Scaneaza cu agenti in paralel:
|
||||
|
||||
1. API AUTH: Verifica ca TOATE rutele din src/app/api/ au auth check
|
||||
(middleware matcher + requireAuth fallback)
|
||||
2. SQL INJECTION: Cauta $queryRaw/$executeRaw cu string concatenation
|
||||
3. INPUT VALIDATION: Verifica sanitizarea pe toate endpoint-urile
|
||||
4. SECRETS: Cauta credentials hardcoded, env vars expuse in client
|
||||
5. ERROR HANDLING: Catch goale, stack traces in responses
|
||||
6. RACE CONDITIONS: Write operations concurente fara locks
|
||||
7. DATA INTEGRITY: Upsert-uri care pot suprascrie date
|
||||
|
||||
Grupeaza in: CRITICAL / IMPORTANT / NICE-TO-HAVE
|
||||
Pentru fiecare: fisier, linia, problema, solutia propusa.
|
||||
NU aplica fix-uri fara sa le listezi mai intai.
|
||||
npx next build TREBUIE sa treaca dupa fiecare fix.`,
|
||||
},
|
||||
{
|
||||
id: "security-module",
|
||||
title: "Audit securitate pe modul",
|
||||
description: "Review de securitate focusat pe un singur modul",
|
||||
tags: ["regular", "security"],
|
||||
variables: ["MODULE_NAME"],
|
||||
prompt: `Fa un audit de securitate pe modulul {MODULE_NAME}.
|
||||
|
||||
Citeste CLAUDE.md si docs/MODULE-MAP.md.
|
||||
|
||||
Verifica:
|
||||
1. API routes folosite de modul — au auth? Input validation?
|
||||
2. Prisma queries — SQL injection posibil?
|
||||
3. User input — sanitizat inainte de stocare/afisare?
|
||||
4. File uploads (daca exista) — validare tip/dimensiune?
|
||||
5. Storage operations — race conditions la concurrent access?
|
||||
6. Error handling — erori silentioase? Stack traces expuse?
|
||||
7. Cross-module deps — sunt corecte si necesare?
|
||||
|
||||
Raporteaza gasirile cu: fisier, linia, severitate, fix propus.`,
|
||||
},
|
||||
{
|
||||
id: "testing-hardcore",
|
||||
title: "Testing hardcore",
|
||||
description: "Edge cases, stress testing, error scenarios pentru un modul",
|
||||
tags: ["periodic", "testing"],
|
||||
variables: ["MODULE_NAME"],
|
||||
prompt: `Vreau sa testez hardcore modulul {MODULE_NAME}.
|
||||
|
||||
Citeste codul modulului complet, apoi gandeste:
|
||||
|
||||
1. EDGE CASES: Ce se intampla cu input gol? Cu caractere speciale (diacritice, emoji)? Cu valori extreme (numar foarte mare, string foarte lung)?
|
||||
|
||||
2. CONCURRENT ACCESS: Ce se intampla daca 2 useri fac aceeasi operatie simultan? Race conditions la write/update/delete?
|
||||
|
||||
3. ERROR PATHS: Ce se intampla daca DB-ul e down? Daca API-ul extern nu raspunde? Daca sesiunea expira mid-operation?
|
||||
|
||||
4. DATA INTEGRITY: Pot pierde date? Pot crea duplicate? Pot suprascrie datele altcuiva?
|
||||
|
||||
5. UI STATE: Ce se intampla daca user-ul da click dublu pe buton? Daca navigheaza away in timpul unui save? Daca face refresh?
|
||||
|
||||
6. STORAGE: Ce se intampla cu date legacy (format vechi)? Cu valori null/undefined in JSON?
|
||||
|
||||
Pentru fiecare problema gasita: descrie scenariul, impactul, si propune fix.
|
||||
Aplica doar ce e CRITICAL dupa aprobare.`,
|
||||
},
|
||||
{
|
||||
id: "performance-audit",
|
||||
title: "Audit performanta",
|
||||
description: "Identificare bottleneck-uri, optimizare queries, bundle size",
|
||||
tags: ["periodic", "performance"],
|
||||
variables: ["MODULE_NAME"],
|
||||
prompt: `Analizeaza performanta modulului {MODULE_NAME}.
|
||||
|
||||
Citeste CLAUDE.md (Storage Performance Rules) si codul modulului.
|
||||
|
||||
Verifica:
|
||||
1. N+1 QUERIES: storage.list() + get() in loop? Ar trebui exportAll()
|
||||
2. LARGE PAYLOADS: Se incarca date inutile? lightweight: true folosit?
|
||||
3. RE-RENDERS: useEffect-uri care trigger-uiesc re-render excesiv?
|
||||
4. BUNDLE SIZE: Import-uri heavy (librarii intregi vs tree-shaking)?
|
||||
5. API CALLS: Request-uri redundante? Lipseste caching?
|
||||
6. DB QUERIES: Lipsesc indexuri? SELECT * in loc de select specific?
|
||||
7. MEMORY: Global singletons care cresc nelimitat? Cache fara TTL?
|
||||
|
||||
Propune optimizarile ordonate dupa impact.
|
||||
Aplica doar dupa aprobare.`,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "session",
|
||||
label: "Sesiune & Continuare",
|
||||
icon: <RefreshCw className="size-4" />,
|
||||
description: "Prompturi pentru inceperea sau continuarea sesiunilor de lucru",
|
||||
prompts: [
|
||||
{
|
||||
id: "continue-tasklist",
|
||||
title: "Continuare din sesiunea anterioara",
|
||||
description: "Reia lucrul de unde am ramas, cu verificare task list",
|
||||
tags: ["regular", "session"],
|
||||
prompt: `Continuam din sesiunea anterioara.
|
||||
|
||||
Citeste CLAUDE.md, MEMORY.md si docs/MODULE-MAP.md.
|
||||
Verifica memory/ pentru context despre ce s-a lucrat recent.
|
||||
|
||||
Apoi:
|
||||
1. Citeste ROADMAP.md (daca exista) pentru task list-ul curent
|
||||
2. Verifica git log --oneline -20 sa vezi ce s-a comis recent
|
||||
3. Verifica git status sa vezi daca sunt schimbari uncommited
|
||||
4. Rezuma ce s-a facut si ce a ramas
|
||||
5. Intreaba-ma cum vreau sa continuam
|
||||
|
||||
Nu incepe sa lucrezi fara confirmare.
|
||||
npx next build TREBUIE sa treaca inainte de orice schimbare.`,
|
||||
},
|
||||
{
|
||||
id: "fresh-session",
|
||||
title: "Sesiune noua — orientare",
|
||||
description: "Prima sesiune sau sesiune dupa pauza lunga — ia-ti bearings",
|
||||
tags: ["regular", "session"],
|
||||
prompt: `Sesiune noua pe ArchiTools.
|
||||
|
||||
Citeste in ordine:
|
||||
1. CLAUDE.md (context proiect + reguli)
|
||||
2. memory/MEMORY.md (index memorii)
|
||||
3. Fiecare fisier din memory/ (context sesiuni anterioare)
|
||||
4. git log --oneline -20 (activitate recenta)
|
||||
5. git status (stare curenta)
|
||||
6. docs/MODULE-MAP.md (harta module)
|
||||
|
||||
Dupa ce ai citit tot, da-mi un rezumat de 5-10 randuri:
|
||||
- Ce s-a facut recent
|
||||
- Ce e in progress / neterminat
|
||||
- Ce probleme sunt cunoscute
|
||||
- Recomandarea ta pentru ce sa facem azi
|
||||
|
||||
Asteapta confirmarea mea inainte de a incepe.`,
|
||||
},
|
||||
{
|
||||
id: "review-refactor",
|
||||
title: "Code review & refactoring",
|
||||
description: "Review si curatare cod pe o zona specifica",
|
||||
tags: ["periodic", "review"],
|
||||
variables: ["TARGET"],
|
||||
prompt: `Fa code review pe: {TARGET}
|
||||
|
||||
Citeste CLAUDE.md si codul tinta complet.
|
||||
|
||||
Verifica:
|
||||
1. PATTERN COMPLIANCE: Urmeaza conventiile din CLAUDE.md?
|
||||
2. TYPE SAFETY: TypeScript strict — sunt tipuri corecte? Null checks?
|
||||
3. ERROR HANDLING: Catch blocks complete? Promise-uri handled?
|
||||
4. NAMING: English code, Romanian UI? Consistent cu restul?
|
||||
5. COMPLEXITY: Functii prea lungi? Logica duplicata?
|
||||
6. SECURITY: Input validation? Auth checks?
|
||||
7. PERFORMANCE: N+1 queries? Re-renders inutile?
|
||||
|
||||
Raporteaza gasirile ordonate dupa severitate.
|
||||
NU aplica refactoring fara listarea schimbarilor propuse si aprobare.
|
||||
Refactoring-ul trebuie sa fie minimal — nu rescrie ce functioneaza.`,
|
||||
},
|
||||
{
|
||||
id: "full-audit",
|
||||
title: "Audit complet codebase",
|
||||
description: "Scanare completa: cod mort, consistenta, securitate, documentatie",
|
||||
tags: ["periodic", "audit"],
|
||||
oneTime: true,
|
||||
prompt: `Scopul acestei sesiuni este un audit complet al codebase-ului ArchiTools cu 3 obiective:
|
||||
|
||||
1. REVIEW & CLEANUP: Cod mort, dependinte neutilizate, TODO/FIXME, consistenta module
|
||||
2. SIGURANTA IN PRODUCTIE: SQL injection, auth gaps, race conditions, data integrity
|
||||
3. DOCUMENTATIE: CLAUDE.md actualizat, docs/ la zi, memory/ updatat
|
||||
|
||||
Citeste CLAUDE.md si MEMORY.md inainte de orice.
|
||||
Foloseste agenti Explore in paralel (minim 5 simultan) pentru scanare.
|
||||
|
||||
Grupeaza gasirile in: CRITICAL / IMPORTANT / NICE-TO-HAVE
|
||||
NU modifica cod fara sa listezi mai intai toate schimbarile propuse.
|
||||
npx next build TREBUIE sa treaca dupa fiecare schimbare.
|
||||
Commit frecvent cu mesaje descriptive.`,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "docs",
|
||||
label: "Documentatie & Meta",
|
||||
icon: <FileText className="size-4" />,
|
||||
description: "Update documentatie, CLAUDE.md, memory, si meta-prompting",
|
||||
prompts: [
|
||||
{
|
||||
id: "update-claudemd",
|
||||
title: "Actualizeaza CLAUDE.md",
|
||||
description: "Sincronizeaza CLAUDE.md cu starea actuala a codului",
|
||||
tags: ["periodic", "docs"],
|
||||
prompt: `CLAUDE.md trebuie actualizat sa reflecte starea curenta a proiectului.
|
||||
|
||||
Pasi:
|
||||
1. Citeste CLAUDE.md curent
|
||||
2. Verifica fiecare sectiune contra codului real:
|
||||
- Module table: sunt toate modulele? Versiuni corecte?
|
||||
- Stack: versiuni la zi?
|
||||
- Conventions: se respecta?
|
||||
- Common Pitfalls: mai sunt relevante? Lipsesc altele noi?
|
||||
- Infrastructure: porturi/servicii corecte?
|
||||
3. Citeste docs/MODULE-MAP.md — e la zi?
|
||||
4. Citeste docs/ARCHITECTURE-QUICK.md — e la zi?
|
||||
|
||||
Propune schimbarile necesare inainte de a le aplica.
|
||||
Target: CLAUDE.md sub 200 linii, informatii derivabile din cod mutate in docs/.`,
|
||||
},
|
||||
{
|
||||
id: "update-memory",
|
||||
title: "Actualizeaza memory/",
|
||||
description: "Curata memorii vechi si adauga context nou",
|
||||
tags: ["periodic", "meta"],
|
||||
prompt: `Verifica si actualizeaza memory/ files.
|
||||
|
||||
Citeste memory/MEMORY.md si fiecare fisier indexat.
|
||||
|
||||
Pentru fiecare memorie:
|
||||
1. E inca relevanta? Daca nu, sterge-o.
|
||||
2. Informatia e la zi? Daca nu, actualizeaz-o.
|
||||
3. Informatia e derivabila din cod? Daca da, sterge-o (redundanta).
|
||||
|
||||
Adauga memorii NOI pentru:
|
||||
- Decizii arhitecturale recente care nu sunt in CLAUDE.md
|
||||
- Feedback-ul meu din aceasta sesiune (daca am corectat ceva)
|
||||
- Starea task-urilor in progress
|
||||
|
||||
NU salva in memory: cod, structura fisierelor, git history — astea se pot citi direct.`,
|
||||
},
|
||||
{
|
||||
id: "improve-prompts",
|
||||
title: "Imbunatateste prompturile",
|
||||
description: "Meta-prompt: analizeaza si rafineaza prompturile din aceasta pagina",
|
||||
tags: ["periodic", "meta"],
|
||||
prompt: `Citeste codul paginii /prompts (src/app/(modules)/prompts/page.tsx).
|
||||
|
||||
Analizeaza fiecare prompt din CATEGORIES:
|
||||
1. E clar si specific? Lipseste context?
|
||||
2. Pasii sunt in ordine logica?
|
||||
3. Include safety nets (build check, aprobare)?
|
||||
4. E prea lung/scurt?
|
||||
5. Variabilele sunt utile?
|
||||
|
||||
Apoi gandeste: ce prompturi noi ar fi utile bazat pe:
|
||||
- Tipurile de task-uri care apar frecvent in git log
|
||||
- Module care sunt modificate des
|
||||
- Greseli care se repeta (din memory/ feedback)
|
||||
|
||||
Propune imbunatatiri si prompturi noi. Aplica dupa aprobare.`,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "quick",
|
||||
label: "Quick Actions",
|
||||
icon: <Zap className="size-4" />,
|
||||
description: "Prompturi scurte pentru actiuni rapide si frecvente",
|
||||
prompts: [
|
||||
{
|
||||
id: "quick-build",
|
||||
title: "Verifica build",
|
||||
description: "Build check rapid",
|
||||
tags: ["quick"],
|
||||
prompt: `Ruleaza npx next build si raporteaza rezultatul. Daca sunt erori, propune fix-uri.`,
|
||||
},
|
||||
{
|
||||
id: "deploy-prep",
|
||||
title: "Pregatire deploy",
|
||||
description: "Checklist complet inainte de push la productie",
|
||||
tags: ["regular", "deploy"],
|
||||
prompt: `Pregateste deploy-ul pe productie (tools.beletage.ro).
|
||||
|
||||
Checklist:
|
||||
1. git status — totul comis? Fisiere untracked suspecte?
|
||||
2. npx next build — zero erori?
|
||||
3. docker-compose.yml — env vars noi necesare?
|
||||
4. prisma/schema.prisma — s-a schimbat? Necesita migrate?
|
||||
5. middleware.ts — rute noi excluse daca e cazul?
|
||||
6. Verifica ca nu sunt credentials hardcoded in cod
|
||||
7. git log --oneline -5 — commit messages descriptive?
|
||||
|
||||
Daca totul e ok, confirma "Ready to push".
|
||||
Daca sunt probleme, listeaza-le cu fix propus.
|
||||
|
||||
IMPORTANT: Dupa push, deploy-ul e MANUAL in Portainer.
|
||||
Daca schema Prisma s-a schimbat, trebuie migrate pe server.`,
|
||||
},
|
||||
{
|
||||
id: "debug-unknown",
|
||||
title: "Debug eroare necunoscuta",
|
||||
description: "Investigheaza o eroare fara cauza evidenta",
|
||||
tags: ["regular", "debug"],
|
||||
variables: ["ERROR_DESCRIPTION"],
|
||||
prompt: `Am o eroare: {ERROR_DESCRIPTION}
|
||||
|
||||
Investigheaza:
|
||||
1. Citeste stack trace-ul (daca exista) — gaseste fisierul root cause
|
||||
2. Citeste codul relevant — nu ghici, verifica
|
||||
3. Cauta pattern-uri similare in codebase (grep)
|
||||
4. Verifica git log recent — s-a schimbat ceva care ar cauza asta?
|
||||
5. Verifica env vars — lipseste ceva?
|
||||
6. Verifica Prisma schema — model-ul e in sync?
|
||||
|
||||
Dupa investigatie:
|
||||
- Explica cauza root (nu simptomul)
|
||||
- Propune fix minim
|
||||
- Aplica fix dupa aprobare
|
||||
- npx next build TREBUIE sa treaca`,
|
||||
},
|
||||
{
|
||||
id: "quick-deps",
|
||||
title: "Update dependinte",
|
||||
description: "Verifica si actualizeaza package.json",
|
||||
tags: ["quick"],
|
||||
prompt: `Verifica daca sunt update-uri disponibile pentru dependintele din package.json.
|
||||
|
||||
Ruleaza: npm outdated
|
||||
Listeaza ce se poate actualiza safe (minor/patch).
|
||||
NU actualiza major versions fara discutie.
|
||||
Dupa update: npx next build TREBUIE sa treaca.`,
|
||||
},
|
||||
{
|
||||
id: "quick-git-cleanup",
|
||||
title: "Git cleanup",
|
||||
description: "Verifica starea repo-ului si curata",
|
||||
tags: ["quick"],
|
||||
prompt: `Verifica starea repo-ului:
|
||||
1. git status — fisiere uncommited?
|
||||
2. git log --oneline -10 — commit-uri recente ok?
|
||||
3. Fisiere untracked suspecte? (.env, tmp files, build artifacts)
|
||||
4. .gitignore — lipseste ceva?
|
||||
|
||||
Propune cleanup daca e nevoie.`,
|
||||
},
|
||||
{
|
||||
id: "quick-type-check",
|
||||
title: "Verificare tipuri modul",
|
||||
description: "Verifica typesafety pe un modul specific",
|
||||
tags: ["quick"],
|
||||
variables: ["MODULE_NAME"],
|
||||
prompt: `Citeste src/modules/{MODULE_NAME}/types.ts si verifica:
|
||||
1. Toate interfetele sunt folosite? (grep imports)
|
||||
2. Sunt tipuri any sau unknown neutipizate?
|
||||
3. Optional fields corect marcate cu ?
|
||||
4. Consistenta cu Prisma schema (daca modulul foloseste DB direct)
|
||||
Raporteaza rapid ce nu e in regula.`,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
// ─── Copy button component ─────────────────────────────────────────
|
||||
|
||||
function CopyButton({ text, className }: { text: string; className?: string }) {
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
const handleCopy = useCallback(async () => {
|
||||
await navigator.clipboard.writeText(text);
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
}, [text]);
|
||||
|
||||
return (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon-xs"
|
||||
onClick={handleCopy}
|
||||
className={className}
|
||||
title="Copiaza"
|
||||
>
|
||||
{copied ? <Check className="size-3 text-green-500" /> : <Copy className="size-3" />}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
// ─── Prompt Card ───────────────────────────────────────────────────
|
||||
|
||||
function PromptCard({ prompt }: { prompt: PromptTemplate }) {
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
const [done, setDone] = useState(false);
|
||||
|
||||
// Persist one-time completion in localStorage
|
||||
useEffect(() => {
|
||||
if (prompt.oneTime) {
|
||||
const stored = localStorage.getItem(`prompt-done-${prompt.id}`);
|
||||
if (stored === "true") setDone(true);
|
||||
}
|
||||
}, [prompt.id, prompt.oneTime]);
|
||||
|
||||
const toggleDone = useCallback(() => {
|
||||
const next = !done;
|
||||
setDone(next);
|
||||
localStorage.setItem(`prompt-done-${prompt.id}`, String(next));
|
||||
}, [done, prompt.id]);
|
||||
|
||||
return (
|
||||
<Card className={`transition-all ${done ? "opacity-50" : ""}`}>
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
{prompt.oneTime && (
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={done}
|
||||
onChange={toggleDone}
|
||||
className="size-4 rounded accent-primary cursor-pointer"
|
||||
title={done ? "Marcheaza ca nefacut" : "Marcheaza ca facut"}
|
||||
/>
|
||||
)}
|
||||
<button
|
||||
onClick={() => setExpanded(!expanded)}
|
||||
className="flex items-center gap-1 text-sm font-semibold hover:text-primary transition-colors text-left"
|
||||
>
|
||||
{expanded ? <ChevronDown className="size-3.5 shrink-0" /> : <ChevronRight className="size-3.5 shrink-0" />}
|
||||
{prompt.title}
|
||||
</button>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground ml-5">{prompt.description}</p>
|
||||
<div className="flex flex-wrap gap-1 mt-2 ml-5">
|
||||
{prompt.tags.map((tag) => (
|
||||
<Badge key={tag} variant="secondary" className="text-[10px] px-1.5 py-0">
|
||||
{tag}
|
||||
</Badge>
|
||||
))}
|
||||
{prompt.variables?.map((v) => (
|
||||
<Badge key={v} variant="outline" className="text-[10px] px-1.5 py-0 border-amber-500/50 text-amber-600 dark:text-amber-400">
|
||||
{`{${v}}`}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<CopyButton text={prompt.prompt} className="shrink-0 mt-0.5" />
|
||||
</div>
|
||||
|
||||
{expanded && (
|
||||
<div className="mt-3 ml-5">
|
||||
<div className="relative group">
|
||||
<pre className="text-xs bg-muted/50 border rounded-md p-3 whitespace-pre-wrap font-mono leading-relaxed overflow-x-auto max-h-[500px] overflow-y-auto">
|
||||
{prompt.prompt}
|
||||
</pre>
|
||||
<CopyButton
|
||||
text={prompt.prompt}
|
||||
className="absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity bg-background/80"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
// ─── Stats bar ─────────────────────────────────────────────────────
|
||||
|
||||
function StatsBar() {
|
||||
const totalPrompts = CATEGORIES.reduce((s, c) => s + c.prompts.length, 0);
|
||||
const oneTimePrompts = CATEGORIES.reduce(
|
||||
(s, c) => s + c.prompts.filter((p) => p.oneTime).length,
|
||||
0,
|
||||
);
|
||||
return (
|
||||
<div className="flex items-center gap-4 text-xs text-muted-foreground">
|
||||
<span className="flex items-center gap-1"><Brain className="size-3" /> {totalPrompts} prompturi</span>
|
||||
<span className="flex items-center gap-1"><ListChecks className="size-3" /> {oneTimePrompts} one-time</span>
|
||||
<span className="flex items-center gap-1"><Wrench className="size-3" /> {CATEGORIES.length} categorii</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ─── Best practices sidebar ────────────────────────────────────────
|
||||
|
||||
const BEST_PRACTICES = [
|
||||
{ icon: <Rocket className="size-3" />, text: "Incepe cu CLAUDE.md — da context inainte de task" },
|
||||
{ icon: <Brain className="size-3" />, text: "Cere plan inainte de implementare" },
|
||||
{ icon: <Shield className="size-3" />, text: "npx next build dupa fiecare schimbare" },
|
||||
{ icon: <Bug className="size-3" />, text: "Nu refactoriza cand faci bugfix" },
|
||||
{ icon: <Sparkles className="size-3" />, text: "O sesiune = un obiectiv clar" },
|
||||
{ icon: <TestTube className="size-3" />, text: "Verifica-ti munca: teste, build, manual" },
|
||||
{ icon: <ListChecks className="size-3" />, text: "Listeaza schimbarile inainte de a le aplica" },
|
||||
{ icon: <RefreshCw className="size-3" />, text: "Actualizeaza memory/ la sfarsit de sesiune" },
|
||||
{ icon: <Zap className="size-3" />, text: "/clear intre task-uri diferite" },
|
||||
{ icon: <Terminal className="size-3" />, text: "Dupa 2 corectii → /clear + prompt mai bun" },
|
||||
];
|
||||
|
||||
// ─── Main Page ─────────────────────────────────────────────────────
|
||||
|
||||
export default function PromptsPage() {
|
||||
const [search, setSearch] = useState("");
|
||||
|
||||
const filtered = CATEGORIES.map((cat) => ({
|
||||
...cat,
|
||||
prompts: cat.prompts.filter(
|
||||
(p) =>
|
||||
!search ||
|
||||
p.title.toLowerCase().includes(search.toLowerCase()) ||
|
||||
p.description.toLowerCase().includes(search.toLowerCase()) ||
|
||||
p.tags.some((t) => t.toLowerCase().includes(search.toLowerCase())),
|
||||
),
|
||||
})).filter((cat) => cat.prompts.length > 0);
|
||||
|
||||
return (
|
||||
<div className="max-w-5xl mx-auto p-6 space-y-6">
|
||||
{/* Header */}
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold flex items-center gap-2">
|
||||
<Terminal className="size-6" />
|
||||
Claude Code Prompts
|
||||
</h1>
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
Biblioteca de prompturi optimizate pentru ArchiTools. Click pe titlu pentru a vedea, buton pentru a copia.
|
||||
</p>
|
||||
<StatsBar />
|
||||
</div>
|
||||
|
||||
{/* Search */}
|
||||
<div className="flex gap-3">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Cauta prompt... (ex: bugfix, security, parcel-sync)"
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
className="flex-1 h-9 rounded-md border bg-background px-3 text-sm placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring"
|
||||
/>
|
||||
{search && (
|
||||
<Button variant="ghost" size="sm" onClick={() => setSearch("")}>
|
||||
Sterge
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-[1fr_240px] gap-6">
|
||||
{/* Main content */}
|
||||
<Tabs defaultValue={CATEGORIES[0]?.id} className="w-full">
|
||||
<TabsList className="w-full flex flex-wrap h-auto gap-1 bg-transparent p-0 mb-4">
|
||||
{filtered.map((cat) => (
|
||||
<TabsTrigger
|
||||
key={cat.id}
|
||||
value={cat.id}
|
||||
className="flex items-center gap-1.5 text-xs data-[state=active]:bg-primary data-[state=active]:text-primary-foreground rounded-md px-3 py-1.5 border"
|
||||
>
|
||||
{cat.icon}
|
||||
{cat.label}
|
||||
<Badge variant="secondary" className="text-[10px] px-1 py-0 ml-1">
|
||||
{cat.prompts.length}
|
||||
</Badge>
|
||||
</TabsTrigger>
|
||||
))}
|
||||
</TabsList>
|
||||
|
||||
{filtered.map((cat) => (
|
||||
<TabsContent key={cat.id} value={cat.id} className="space-y-3 mt-0">
|
||||
<p className="text-xs text-muted-foreground mb-3">{cat.description}</p>
|
||||
{cat.prompts.map((prompt) => (
|
||||
<PromptCard key={prompt.id} prompt={prompt} />
|
||||
))}
|
||||
</TabsContent>
|
||||
))}
|
||||
</Tabs>
|
||||
|
||||
{/* Sidebar */}
|
||||
<div className="space-y-4">
|
||||
<Card>
|
||||
<CardContent className="p-4">
|
||||
<h3 className="text-sm font-semibold mb-3 flex items-center gap-1.5">
|
||||
<Sparkles className="size-3.5" />
|
||||
Best Practices
|
||||
</h3>
|
||||
<div className="space-y-2.5">
|
||||
{BEST_PRACTICES.map((bp, i) => (
|
||||
<div key={i} className="flex items-start gap-2 text-xs text-muted-foreground">
|
||||
<span className="mt-0.5 shrink-0">{bp.icon}</span>
|
||||
<span>{bp.text}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardContent className="p-4">
|
||||
<h3 className="text-sm font-semibold mb-3 flex items-center gap-1.5">
|
||||
<Terminal className="size-3.5" />
|
||||
Module disponibile
|
||||
</h3>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{MODULES.map((m) => (
|
||||
<Badge key={m} variant="outline" className="text-[10px] px-1.5 py-0 cursor-pointer hover:bg-accent" onClick={() => {
|
||||
navigator.clipboard.writeText(m);
|
||||
}}>
|
||||
{m}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
<p className="text-[10px] text-muted-foreground mt-2">Click pe modul = copiaza numele</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,11 +1,20 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { requireAuth } from "./auth-check";
|
||||
|
||||
const STIRLING_PDF_URL =
|
||||
process.env.STIRLING_PDF_URL ?? "http://10.10.10.166:8087";
|
||||
const STIRLING_PDF_API_KEY =
|
||||
process.env.STIRLING_PDF_API_KEY ?? "cd829f62-6eef-43eb-a64d-c91af727b53a";
|
||||
const STIRLING_PDF_URL = process.env.STIRLING_PDF_URL;
|
||||
const STIRLING_PDF_API_KEY = process.env.STIRLING_PDF_API_KEY;
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
const authErr = await requireAuth(req);
|
||||
if (authErr) return authErr;
|
||||
|
||||
if (!STIRLING_PDF_URL || !STIRLING_PDF_API_KEY) {
|
||||
return NextResponse.json(
|
||||
{ error: "Stirling PDF nu este configurat" },
|
||||
{ status: 503 },
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
// Buffer the full body then forward to Stirling — streaming passthrough
|
||||
// (req.body + duplex:half) is unreliable for large files in Next.js.
|
||||
|
||||
@@ -1,11 +1,20 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { requireAuth } from "../auth-check";
|
||||
|
||||
const STIRLING_PDF_URL =
|
||||
process.env.STIRLING_PDF_URL ?? "http://10.10.10.166:8087";
|
||||
const STIRLING_PDF_API_KEY =
|
||||
process.env.STIRLING_PDF_API_KEY ?? "cd829f62-6eef-43eb-a64d-c91af727b53a";
|
||||
const STIRLING_PDF_URL = process.env.STIRLING_PDF_URL;
|
||||
const STIRLING_PDF_API_KEY = process.env.STIRLING_PDF_API_KEY;
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
const authErr = await requireAuth(req);
|
||||
if (authErr) return authErr;
|
||||
|
||||
if (!STIRLING_PDF_URL || !STIRLING_PDF_API_KEY) {
|
||||
return NextResponse.json(
|
||||
{ error: "Stirling PDF nu este configurat" },
|
||||
{ status: 503 },
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
// Stream body directly to Stirling — avoids FormData re-serialization
|
||||
// failure on large files ("Failed to parse body as FormData")
|
||||
|
||||
@@ -78,7 +78,7 @@ export async function GET(req: Request) {
|
||||
WHERE geom IS NOT NULL
|
||||
AND "layerId" LIKE 'TERENURI%'
|
||||
AND ("cadastralRef" ILIKE ${pattern}
|
||||
OR enrichment::text ILIKE ${'%"NR_CAD":"' + q + '%'})
|
||||
OR enrichment::text ILIKE ${`%"NR_CAD":"${q}%`})
|
||||
ORDER BY "cadastralRef"
|
||||
LIMIT ${limit}
|
||||
` as Array<{
|
||||
|
||||
@@ -11,11 +11,21 @@ import { NextResponse } from "next/server";
|
||||
import { prisma } from "@/core/storage/prisma";
|
||||
import { getAuthSession } from "@/core/auth";
|
||||
|
||||
export async function GET() {
|
||||
async function requireAdmin(): Promise<NextResponse | null> {
|
||||
const session = await getAuthSession();
|
||||
if (!session) {
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
const u = session.user as { role?: string } | undefined;
|
||||
if (u?.role !== "admin") {
|
||||
return NextResponse.json({ error: "Admin access required" }, { status: 403 });
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export async function GET() {
|
||||
const denied = await requireAdmin();
|
||||
if (denied) return denied;
|
||||
|
||||
// Get all sequence counters
|
||||
const counters = await prisma.$queryRaw<
|
||||
@@ -79,10 +89,8 @@ export async function GET() {
|
||||
}
|
||||
|
||||
export async function POST() {
|
||||
const session = await getAuthSession();
|
||||
if (!session) {
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
const denied = await requireAdmin();
|
||||
if (denied) return denied;
|
||||
|
||||
// Delete ALL old counters
|
||||
const deleted = await prisma.$executeRaw`DELETE FROM "RegistrySequence"`;
|
||||
@@ -146,10 +154,8 @@ export async function POST() {
|
||||
* Rewrites the "number" field inside the JSONB value for matching entries.
|
||||
*/
|
||||
export async function PATCH() {
|
||||
const session = await getAuthSession();
|
||||
if (!session) {
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
const denied = await requireAdmin();
|
||||
if (denied) return denied;
|
||||
|
||||
// Map old 3-letter prefixes to new single-letter
|
||||
const migrations: Array<{ old: string; new: string }> = [
|
||||
|
||||
@@ -213,27 +213,33 @@ export async function POST(req: NextRequest) {
|
||||
let claimedSlotId: string | undefined;
|
||||
|
||||
if (isPastMonth && direction === "intrat") {
|
||||
// Try to claim a reserved slot
|
||||
const allEntries = await loadAllEntries(true);
|
||||
const slot = findAvailableReservedSlot(
|
||||
allEntries,
|
||||
company,
|
||||
docDate.getFullYear(),
|
||||
docDate.getMonth(),
|
||||
);
|
||||
// Try to claim a reserved slot — use advisory lock to prevent concurrent claims
|
||||
const lockKey = `reserved:${company}-${docDate.getFullYear()}-${docDate.getMonth()}`;
|
||||
const claimed = await prisma.$transaction(async (tx) => {
|
||||
await tx.$executeRaw`SELECT pg_advisory_xact_lock(hashtext(${lockKey}))`;
|
||||
const allEntries = await loadAllEntries(true);
|
||||
const slot = findAvailableReservedSlot(
|
||||
allEntries,
|
||||
company,
|
||||
docDate.getFullYear(),
|
||||
docDate.getMonth(),
|
||||
);
|
||||
if (!slot) return null;
|
||||
// Delete the placeholder slot within the lock
|
||||
await tx.keyValueStore.delete({
|
||||
where: { namespace_key: { namespace: "registratura", key: slot.id } },
|
||||
});
|
||||
return slot;
|
||||
});
|
||||
|
||||
if (slot) {
|
||||
// Claim the reserved slot — reuse its number
|
||||
registryNumber = slot.number;
|
||||
if (claimed) {
|
||||
registryNumber = claimed.number;
|
||||
registrationType = "reserved-claimed";
|
||||
claimedSlotId = slot.id;
|
||||
|
||||
// Delete the placeholder slot
|
||||
await deleteEntryFromDB(slot.id);
|
||||
claimedSlotId = claimed.id;
|
||||
|
||||
await logAuditEvent({
|
||||
entryId: slot.id,
|
||||
entryNumber: slot.number,
|
||||
entryId: claimed.id,
|
||||
entryNumber: claimed.number,
|
||||
action: "reserved_claimed",
|
||||
actor: actor.id,
|
||||
actorName: actor.name,
|
||||
|
||||
@@ -144,8 +144,8 @@ export async function DELETE(request: NextRequest) {
|
||||
},
|
||||
},
|
||||
})
|
||||
.catch(() => {
|
||||
// Ignore error if item doesn't exist
|
||||
.catch((err: { code?: string }) => {
|
||||
if (err.code !== "P2025") throw err;
|
||||
});
|
||||
} else {
|
||||
// Clear namespace
|
||||
|
||||
@@ -18,16 +18,3 @@ if (process.env.NODE_ENV !== "production")
|
||||
globalForMinio.minioClient = minioClient;
|
||||
|
||||
export const MINIO_BUCKET_NAME = process.env.MINIO_BUCKET_NAME || "tools";
|
||||
|
||||
// Helper to ensure bucket exists
|
||||
export async function ensureBucketExists() {
|
||||
try {
|
||||
const exists = await minioClient.bucketExists(MINIO_BUCKET_NAME);
|
||||
if (!exists) {
|
||||
await minioClient.makeBucket(MINIO_BUCKET_NAME, "eu-west-1");
|
||||
console.log(`Bucket '${MINIO_BUCKET_NAME}' created successfully.`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error checking/creating MinIO bucket:", error);
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -16,7 +16,7 @@ export async function middleware(request: NextRequest) {
|
||||
if (token) {
|
||||
const { pathname } = request.nextUrl;
|
||||
// Portal-only users: redirect to /portal when accessing main app
|
||||
const portalUsers = ["dtiurbe", "d.tiurbe"];
|
||||
const portalUsers = (process.env.PORTAL_ONLY_USERS ?? "dtiurbe,d.tiurbe").split(",").map(s => s.trim().toLowerCase());
|
||||
const tokenEmail = String(token.email ?? "").toLowerCase();
|
||||
const tokenName = String(token.name ?? "").toLowerCase();
|
||||
const isPortalUser = portalUsers.some(
|
||||
|
||||
@@ -54,11 +54,24 @@ type SessionEntry = {
|
||||
|
||||
const globalStore = globalThis as {
|
||||
__epaySessionCache?: Map<string, SessionEntry>;
|
||||
__epayCleanupTimer?: ReturnType<typeof setInterval>;
|
||||
};
|
||||
const sessionCache =
|
||||
globalStore.__epaySessionCache ?? new Map<string, SessionEntry>();
|
||||
globalStore.__epaySessionCache = sessionCache;
|
||||
|
||||
// Periodic cleanup of expired sessions (every 5 minutes, 9-min TTL)
|
||||
if (!globalStore.__epayCleanupTimer) {
|
||||
globalStore.__epayCleanupTimer = setInterval(() => {
|
||||
const now = Date.now();
|
||||
for (const [key, entry] of sessionCache.entries()) {
|
||||
if (now - entry.lastUsed > 9 * 60_000) {
|
||||
sessionCache.delete(key);
|
||||
}
|
||||
}
|
||||
}, 5 * 60_000);
|
||||
}
|
||||
|
||||
const makeCacheKey = (u: string, p: string) =>
|
||||
crypto.createHash("sha256").update(`epay:${u}:${p}`).digest("hex");
|
||||
|
||||
|
||||
@@ -117,27 +117,29 @@ export async function enqueueBatch(
|
||||
const items: QueueItem[] = [];
|
||||
|
||||
for (const input of inputs) {
|
||||
// Create DB record in "queued" status
|
||||
const record = await prisma.cfExtract.create({
|
||||
data: {
|
||||
nrCadastral: input.nrCadastral,
|
||||
nrCF: input.nrCF ?? input.nrCadastral,
|
||||
siruta: input.siruta,
|
||||
judetIndex: input.judetIndex,
|
||||
judetName: input.judetName,
|
||||
uatId: input.uatId,
|
||||
uatName: input.uatName,
|
||||
gisFeatureId: input.gisFeatureId,
|
||||
prodId: input.prodId ?? 14200,
|
||||
status: "queued",
|
||||
version:
|
||||
((
|
||||
await prisma.cfExtract.aggregate({
|
||||
where: { nrCadastral: input.nrCadastral },
|
||||
_max: { version: true },
|
||||
})
|
||||
)._max.version ?? 0) + 1,
|
||||
},
|
||||
// Create DB record in "queued" status — use transaction + advisory lock
|
||||
// to prevent duplicate version numbers from concurrent requests
|
||||
const record = await prisma.$transaction(async (tx) => {
|
||||
await tx.$executeRaw`SELECT pg_advisory_xact_lock(hashtext(${'cfextract:' + input.nrCadastral}))`;
|
||||
const agg = await tx.cfExtract.aggregate({
|
||||
where: { nrCadastral: input.nrCadastral },
|
||||
_max: { version: true },
|
||||
});
|
||||
return tx.cfExtract.create({
|
||||
data: {
|
||||
nrCadastral: input.nrCadastral,
|
||||
nrCF: input.nrCF ?? input.nrCadastral,
|
||||
siruta: input.siruta,
|
||||
judetIndex: input.judetIndex,
|
||||
judetName: input.judetName,
|
||||
uatId: input.uatId,
|
||||
uatName: input.uatName,
|
||||
gisFeatureId: input.gisFeatureId,
|
||||
prodId: input.prodId ?? 14200,
|
||||
status: "queued",
|
||||
version: (agg._max.version ?? 0) + 1,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
items.push({ extractId: record.id, input });
|
||||
@@ -418,7 +420,10 @@ async function processBatch(
|
||||
},
|
||||
);
|
||||
|
||||
// Complete
|
||||
// Complete — require document date from ANCPI for accurate expiry
|
||||
if (!doc.dataDocument) {
|
||||
console.warn(`[epay-queue] Missing dataDocument for extract ${item.extractId}, using download date`);
|
||||
}
|
||||
const documentDate = doc.dataDocument
|
||||
? new Date(doc.dataDocument)
|
||||
: new Date();
|
||||
|
||||
@@ -79,11 +79,24 @@ type SessionEntry = {
|
||||
|
||||
const globalStore = globalThis as {
|
||||
__eterraSessionStore?: Map<string, SessionEntry>;
|
||||
__eterraCleanupTimer?: ReturnType<typeof setInterval>;
|
||||
};
|
||||
const sessionStore =
|
||||
globalStore.__eterraSessionStore ?? new Map<string, SessionEntry>();
|
||||
globalStore.__eterraSessionStore = sessionStore;
|
||||
|
||||
// Periodic cleanup of expired sessions (every 5 minutes, 9-min TTL)
|
||||
if (!globalStore.__eterraCleanupTimer) {
|
||||
globalStore.__eterraCleanupTimer = setInterval(() => {
|
||||
const now = Date.now();
|
||||
for (const [key, entry] of sessionStore.entries()) {
|
||||
if (now - entry.lastUsed > 9 * 60_000) {
|
||||
sessionStore.delete(key);
|
||||
}
|
||||
}
|
||||
}, 5 * 60_000);
|
||||
}
|
||||
|
||||
const makeCacheKey = (u: string, p: string) =>
|
||||
crypto.createHash("sha256").update(`${u}:${p}`).digest("hex");
|
||||
|
||||
|
||||
@@ -16,10 +16,24 @@ export type SyncProgress = {
|
||||
|
||||
type ProgressStore = Map<string, SyncProgress>;
|
||||
|
||||
const g = globalThis as { __parcelSyncProgressStore?: ProgressStore };
|
||||
const g = globalThis as {
|
||||
__parcelSyncProgressStore?: ProgressStore;
|
||||
__progressCleanupTimer?: ReturnType<typeof setInterval>;
|
||||
};
|
||||
const store: ProgressStore = g.__parcelSyncProgressStore ?? new Map();
|
||||
g.__parcelSyncProgressStore = store;
|
||||
|
||||
// Periodic cleanup of stale progress entries (every 30 minutes)
|
||||
if (!g.__progressCleanupTimer) {
|
||||
g.__progressCleanupTimer = setInterval(() => {
|
||||
for (const [jobId, p] of store.entries()) {
|
||||
if (p.status === "done" || p.status === "error") {
|
||||
store.delete(jobId);
|
||||
}
|
||||
}
|
||||
}, 30 * 60_000);
|
||||
}
|
||||
|
||||
export const setProgress = (p: SyncProgress) => store.set(p.jobId, p);
|
||||
export const getProgress = (jobId: string) => store.get(jobId);
|
||||
export const clearProgress = (jobId: string) => store.delete(jobId);
|
||||
|
||||
@@ -237,8 +237,16 @@ export async function syncLayer(
|
||||
},
|
||||
create: item,
|
||||
update: {
|
||||
...item,
|
||||
siruta: item.siruta,
|
||||
inspireId: item.inspireId,
|
||||
cadastralRef: item.cadastralRef,
|
||||
areaValue: item.areaValue,
|
||||
isActive: item.isActive,
|
||||
attributes: item.attributes,
|
||||
geometry: item.geometry,
|
||||
syncRunId: item.syncRunId,
|
||||
updatedAt: new Date(),
|
||||
// enrichment + enrichedAt preserved — not overwritten
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
X,
|
||||
FileText,
|
||||
} from "lucide-react";
|
||||
import { useAuth } from "@/core/auth";
|
||||
import { Button } from "@/shared/components/ui/button";
|
||||
import { Input } from "@/shared/components/ui/input";
|
||||
import { Label } from "@/shared/components/ui/label";
|
||||
@@ -66,6 +67,7 @@ export function CloseGuardDialog({
|
||||
activeDeadlines,
|
||||
onConfirmClose,
|
||||
}: CloseGuardDialogProps) {
|
||||
const { user } = useAuth();
|
||||
const [search, setSearch] = useState("");
|
||||
const [selectedEntryId, setSelectedEntryId] = useState("");
|
||||
const [resolution, setResolution] = useState<ClosureResolution>("finalizat");
|
||||
@@ -130,7 +132,7 @@ export function CloseGuardDialog({
|
||||
onConfirmClose({
|
||||
resolution,
|
||||
reason: reason.trim(),
|
||||
closedBy: "Utilizator", // TODO: replace with SSO identity
|
||||
closedBy: user?.name ?? "Utilizator",
|
||||
closedAt: new Date().toISOString(),
|
||||
linkedEntryId: selectedEntryId || undefined,
|
||||
linkedEntryNumber: selectedEntry?.number,
|
||||
|
||||
Reference in New Issue
Block a user