Commit Graph

23 Commits

Author SHA1 Message Date
admin c8f0306abb feat(M4): Tochka bank integration — credentials + issue invoice
Backend:
- lib/crypto.ts — AES-256-GCM encrypt/decrypt for secret storage (TOCHKA_JWT_KEY)
- modules/tochka/client.ts — typed HTTP client with sandbox/prod baseURL,
  auto Bearer auth from decrypted JWT, 30s timeout
  endpoints: getCustomers, getAccounts, createInvoice, getInvoicePaymentStatus, getInvoicePdf
- modules/tochka/routes.ts — credentials CRUD + GET test-connection (lists customers)
  JWT never returned in responses
- modules/tochka/issue.routes.ts:
  - POST /api/documents/:id/issue-tochka — creates invoice in Tochka, saves
    documentId+environment, advances status draft→issued
  - GET /api/documents/:id/tochka/status — payment status check
  - GET /api/documents/:id/tochka/pdf — proxy bank's PDF
  Selects credential prod-first, falls back to sandbox

Frontend:
- api.ts: TochkaEnv, TochkaCredential, TochkaCustomer types
- CompanyEdit > Integrations tab: full UI — list creds, add for sandbox/prod,
  «Проверить» button calls test-connection (validates JWT works), update token
  / archive, paste-friendly defaults (sandbox.jwt.token preset for sandbox)
- DocumentEdit (when docType=invoice): tochka-panel
  - if not issued: «🏦 Выставить через Точку» button
  - if issued: shows env+documentId, «PDF из банка» and «Статус оплаты» buttons

Sandbox flow: create sandbox credential with token «sandbox.jwt.token» and
any customerCode/accountCode → test connection → issue invoice.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 14:29:37 +03:00
admin f95fa7c254 feat(projects): explicit organization (executor) selector
Backend:
- POST /api/projects accepts optional organizationId; default = active org
- GET /api/projects (list) and GET /:id include organization {id,name,shortName}
- /:id and /:id/documents no longer filter by active org — direct link works
  across organizations; UI offers to switch active to project's org

Frontend:
- Project create modal: «Фирма (исполнитель)» dropdown, default = active
- Projects list shows «Фирма» column when user has >1 organization
- ProjectEdit shows org-banner with project's organization;
  if active org differs, banner has «переключить активную на эту →» button
- ProjectEdit fetches BankAccounts from project's org (not active)
- Bumped clients fetch limit to 1000 to match API max

Org of an existing project cannot be changed (would orphan documents/lines);
to switch fenced — create new project under desired org.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 14:11:27 +03:00
admin 8b7fe30b76 feat: simplified document editor + optional blocks
Schema:
- Block: new optional fields `optional` (boolean) and `optionalLabel` (string)
- DocBody: new field `disabledBlockIds` — IDs of optional blocks turned off on this instance
- Renderer skips blocks whose id is in disabledBlockIds

Template authoring (BlocksEditor):
- Each block card has «Опциональный» checkbox + custom label input
- Optional blocks visually highlighted (amber border)

Document editing (DocumentEdit):
- New «Расширенный режим» toggle in header (default off → simple mode)
- Simple mode: only document header, lines, and «Дополнительные пункты»
  section listing optional blocks as checkboxes
- Advanced mode: also shows full BlocksEditor (previous behavior)
- Toggling optional block updates body.disabledBlockIds; renderer/PDF reflects it

Workflow: автор шаблона помечает спорные блоки как опциональные → пользователь
при создании документа просто отмечает галочки, не залезая в контент.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 14:01:57 +03:00
admin a5330fd46e fix(api): bump clients list limit max from 200 to 1000 (ProjectEdit fetches all)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 13:51:54 +03:00
admin 90cebb0e0f fix: project create UX + project picker in DocumentEdit
- Projects.tsx: pre-validate empty name, show fieldErrors on Field
  (root cause of '400 validation_error' was empty name silently rejected)
- ProjectPicker component for DocumentEdit; selecting a project autofills
  default client when no client is yet set

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 13:48:42 +03:00
admin b2c221e643 feat: Projects + stronger LLM placeholder substitution
Backend:
- Project model with defaults: defaultClient, defaultTemplate, defaultBankAccount
- Document gets optional projectId (FK + index)
- Migration 2_projects: enum ProjectStatus + Project table + Document.projectId
- API: /api/projects CRUD, GET /api/projects/:id/documents
- documents/routes filter by projectId

LLM prompt:
- Concrete preamble example: «ООО «...», в лице директора ... , ИП ..., ОГРНИП ...»
  → {{customer.name}}, {{customer.signatoryPosition}} {{customer.signatoryName}},
    {{executor.name}}, {{executor.ogrn}}
- Expanded placeholder list (signatoryName/Position для customer, ogrn etc)
- Side-role detection: «Заказчик» vs «Исполнитель» map to customer/executor

Frontend:
- /projects (list) and /projects/:id (defaults form + documents list under project)
- Nav: «Проекты» first
- DocumentEdit reads projectId from URL, presets default client from project,
  saves projectId with the document

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 13:17:07 +03:00
admin 973d85c1dd fix(recognize): party blocks belong before signatures, not at top
- Prompt: explicit rule that party blocks always go at the end, before signatures;
  preamble in the beginning is a paragraph, not party
- Postprocess: deterministic reorder pulls party blocks to the position right
  before the last signatures block (or to the end if no signatures)

Existing imported templates can be re-imported, or party blocks moved manually
in the editor (↑↓ buttons). New imports come out in correct order.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 12:55:04 +03:00
admin 2a03c1f450 fix(api): pin pdf-parse to 1.1.1 (2.x rewrote API to non-function exports)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 12:45:45 +03:00
admin 66c99c7d99 fix(api): use createRequire for pdf-parse (CJS-only, exports map blocks deep path)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 12:40:03 +03:00
admin ca06cd7e58 fix(api): import pdf-parse from lib path (avoid index.js auto-test mode in ESM)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 12:37:47 +03:00
admin 8c29760615 fix(api): pin @fastify/multipart to ^8 for fastify 4 compat
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 11:43:58 +03:00
admin e768d30fb6 feat: import DOCX/PDF/scanned templates via DeepSeek recognition
Backend pipeline:
- POST /api/templates/import (multipart, max 25 MB)
- extract.ts: DOCX→mammoth, PDF→pdf-parse, fallback to OCR via tesseract+poppler-utils
  (pdftoppm renders pages to PNG, tesseract reads with rus+eng)
- deepseek.ts: chat completions client with strict JSON response_format
- recognize.ts: structured prompt that produces simplified DocBody (string text),
  postprocessor wraps text in TipTap-compatible JSON, validates with zod schema
- prompt enforces placeholder substitution: {{customer.*}}, {{executor.*}},
  {{contract.number}}, {{contract.date}}, {{today}}
- error codes: NO_OCR / NO_DEEPSEEK_KEY / UNSUPPORTED_MIME / INVALID_DOC_BODY

Dockerfile: apk add tesseract-ocr (+rus +eng data), poppler-utils, imagemagick

Frontend:
- Templates page: ⤴ Загрузить документ → file picker (.docx,.pdf,.png,.jpg)
- doc type selector (contract/invoice/act/upd)
- import-banner with spinner shows uploading→analyzing stages
- on success navigates to /templates/:id (TemplateEdit) for review

Reuses DEEPSEEK_API_KEY pattern from Hall-planer.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 11:40:28 +03:00
admin 624d378bb5 feat: DaData ИНН lookup for clients and companies
- New API endpoint GET /api/lookup/party?inn=... — proxies to DaData API,
  returns parsed party (name, kpp, ogrn, address, signatory, status)
- Env DADATA_API_KEY (optional) — without it endpoint returns 503/no_dadata_key
- Web: InnLookupButton component shown next to ИНН field in Clients form
  and Company requisites; on click fetches DaData and fills all matching
  fields. Warns if status is not ACTIVE (liquidated, etc.)

Free DaData tier: 10000 requests/day. Get key at
https://dadata.ru/api/find-party/ — paste into DADATA_API_KEY in
docker/.env on queoserver.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 11:16:20 +03:00
admin 524789facc feat: multi-organization support + bank accounts
- Renamed singular /api/organization → CRUD /api/organizations
- New BankAccount table with CRUD under /api/organizations/:orgId/bank-accounts
- TochkaCredential gets optional bankAccountId for future per-account bank API config
- Active organization stored in cookie dm_org, /api/active-organization GET/POST
- activeOrgPlugin resolves req._orgId from cookie (or first non-archived as fallback)
- Migration 1_multiorg: BankAccount table + data backfill from legacy Organization.bank* fields
- Web: new /companies list + /companies/:id with tabs (Реквизиты, Банки и счета, Интеграции stub)
- Web: OrgSwitcher dropdown in header (active org + management link)
- Removed nav "Реквизиты", "Банк" — replaced by "Компании"
- Per-field error highlighting wired up on new forms

Existing organization data backfills cleanly: legacy bank* fields stay readable, but new
BankAccount becomes the source of truth going forward.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 11:08:26 +03:00
admin b28c0463b3 fix(web): allow undefined for Field error prop (exactOptionalPropertyTypes)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 10:50:25 +03:00
admin bf360d6e53 feat(web): per-field error highlighting from API validation issues
ApiError.fieldErrors() returns map field→ru-message; forms set fieldErrors state
on save failure, pass error prop to Field components (red border + inline message),
clear individual error when user edits that field.

Wired up: Clients, Organization. Services и Documents — позже.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 10:49:37 +03:00
admin 747246197a fix(api): treat empty strings as null in optional fields; show issues on web
- zod-utils.ts: optionalRegex/optionalEmail/optionalText with preprocess('' -> null)
- clients/organizations/services schemas now accept empty strings for optional fields
  (controlled inputs naturally produce '' instead of null)
- ApiError.prettyMessage() unfolds zod fieldErrors so users see which field failed

Reproduce: POST /api/clients with email="" or empty inn returned 400 validation_error.
Now it succeeds (empty stays null), and any real validation error names the field.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 08:42:03 +03:00
admin 9807d47c8d feat(M3): contracts editor, templates, PDF render via Puppeteer/Chromium
Backend:
- numbers.ts — per-org per-doctype per-year sequential numbering (ДГ-2026/001, СЧ-2026/001…)
- money.ts — line totals + Russian rubInWords helper
- documents/routes.ts — CRUD with transactional lines bulk-replace, status changes, history endpoint for client-line autocomplete
- templates/routes.ts — CRUD + instantiate (clones template body into new draft document)
- shared/render/toHtml.ts — block→HTML renderer with placeholder substitution ({{customer.inn}}, {{contract.number}}, {{today}}…)
- documents/pdf.ts — Puppeteer-based PDF rendering with auto-detected Chromium executable
- documents/pdf.routes.ts — GET /:id/preview (HTML) and GET /:id/pdf
- Dockerfile.api — added apk chromium + cyrillic fonts

Web:
- api.ts — Document, DocumentTemplate, Block, LineHistoryItem types
- BlocksEditor — generic block list with reorder/add/remove and per-block forms (heading, party, services_table, totals, terms, signatures, custom_text, page_break)
- LinesEditor — services rows with auto sumCents, "from catalog" picker, "from history by client" panel
- ClientPicker — reusable client dropdown
- pages: Documents list, DocumentEdit (new+existing), Templates list, TemplateEdit
- richtext.ts — plain↔TipTap-JSON conversion (no TipTap yet, just keeps the format compatible)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 08:29:44 +03:00
admin 0722a25845 fix(auth): resource key follows Queo convention 'doc.queo.ru' (subdomain), not 'doc_manager'
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 21:48:30 +03:00
admin d9bd6fc68f fix(web): no-cache for index.html so new bundle hashes propagate
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 21:44:35 +03:00
admin 4965fdd60f fix: auth login URL is /login on Auth_server, not /auth/login
GET /auth/login → 404. Auth_server's user-facing login form lives at
GET https://auth.queo.ru/login (rendered by views/login.html), while
POST /auth/login is the JSON API. Web was redirecting to wrong path.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 21:41:41 +03:00
admin de3af9d5fa chore: rename UI Tochka→Bank, prod-ready compose, init migration, Caddy snippet
- UI nav: Точка → Банк (route /bank), label-only — internal Tochka API module unchanged
- compose: drop our own Caddy (queoserver uses host-level Caddy), web-nginx proxies /api/*+/webhooks/* to api:3030 internally; expose only 127.0.0.1:3031
- Dockerfile.api: simpler single-stage with tsx for prod (avoids dist build complexity)
- prisma/migrations/0_init committed via `prisma migrate diff` for `migrate deploy` in prod
- docker/Caddyfile.snippet: copy-paste block for queoserver's /etc/caddy/Caddyfile
- .gitignore: data/ (covers embedded-pg, postgres, caddy data dirs)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 21:31:48 +03:00
admin 4553f63deb init: M1 scaffolding + M2 organization/clients/services CRUD
- monorepo (npm workspaces): apps/api (Fastify+Prisma+TS), apps/web (Vite+React+TS), packages/shared (zod schemas)
- SSO via auth.queo.ru: jose+JWKS plugin, requireDocPermission(viewer|user|admin)
- DEV_BYPASS_AUTH for local development (hard-checked off in production)
- M2: organization upsert, clients CRUD with search, services catalog with soft-delete
- BigInt -> Number serializer for Prisma money columns
- Embedded Postgres + npm run dev:demo for one-command local boot
- Docker compose for queoserver: postgres + api + web (nginx as ingress proxying /api -> api:3030)
- First migration 0_init committed (prisma migrate diff)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 21:24:26 +03:00