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>
This commit is contained in:
admin
2026-05-01 14:01:57 +03:00
parent a5330fd46e
commit 8b7fe30b76
7 changed files with 133 additions and 21 deletions
+13 -2
View File
@@ -36,7 +36,15 @@ export const VAT_PERCENT: Record<VatRate, number> = {
vat_20: 20,
};
const blockBase = z.object({ id: z.string().min(1) });
// Базовые поля для всех блоков:
// - optional: блок помечается как «выбираемый при создании документа»;
// автор шаблона ставит флаг, пользователь при инстансировании видит его как checkbox.
// - optionalLabel: пользовательский лейбл для checkbox (если не задан — берётся стандартное имя типа).
const blockBase = z.object({
id: z.string().min(1),
optional: z.boolean().optional(),
optionalLabel: z.string().max(200).optional(),
});
export const HeadingBlock = blockBase.extend({
type: z.literal('heading'),
@@ -109,7 +117,10 @@ export const DocBody = z.object({
version: z.literal(1),
blocks: z.array(Block),
vars: z.record(z.unknown()).default({}),
// ID блоков, которые на этом инстансе документа выключены пользователем (galочка снята).
// Применимо только к блокам с optional=true; они физически остаются в body, но не попадают в рендер.
disabledBlockIds: z.array(z.string()).default([]),
});
export type DocBody = z.infer<typeof DocBody>;
export const emptyDocBody = (): DocBody => ({ version: 1, blocks: [], vars: {} });
export const emptyDocBody = (): DocBody => ({ version: 1, blocks: [], vars: {}, disabledBlockIds: [] });
+5 -1
View File
@@ -288,7 +288,11 @@ export function renderDocumentHtml(
rubInWords?: (cents: number) => string;
} = {},
): string {
const blocksHtml = body.blocks.map((b) => renderBlock(b, ctx, opts.rubInWords)).join('\n');
const disabled = new Set(body.disabledBlockIds ?? []);
const blocksHtml = body.blocks
.filter((b) => !disabled.has(b.id))
.map((b) => renderBlock(b, ctx, opts.rubInWords))
.join('\n');
const title = opts.title ?? `Документ ${ctx.doc.number}`;
return `<!doctype html>