Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,13 @@ Versioning policy: see [CONTRIBUTING.md](CONTRIBUTING.md#versioning).

## [Unreleased]

## [2.4.4] — 2026-06-30

### Fixed
- **Поле «№ документа» при добавлении документа теперь необязательное (issue #18).** В окне «Добавить документ» номер был помечен `*` и блокировал кнопку «Создать документ», пока его не заполнят. При этом на бэкенде поле и так было необязательным (колонка `number` — nullable, серверная схема принимает `null`) — обязательность держалась только на форме. Теперь номер можно не указывать: документ создаётся без него (форма шлёт `null` для пустого значения), а в реестре пустой номер отображается как «—». Указанный номер по-прежнему ограничен 500 символами.

> Только фронтенд: бэкенд, схема БД и публичный контракт не менялись (поле уже было nullable). Компания и Тип документа остаются обязательными. PATCH согласно [политике версионирования](CONTRIBUTING.md#versioning) — приведение формы к уже заложенной в бэкенде необязательности (как с ИНН в 2.2.1).

## [2.4.3] — 2026-06-30

### Fixed
Expand Down
2 changes: 1 addition & 1 deletion web/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@lotsman/web",
"version": "2.4.3",
"version": "2.4.4",
"license": "BUSL-1.1",
"private": true,
"type": "module",
Expand Down
3 changes: 2 additions & 1 deletion web/src/features/registry/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,8 @@ export interface BulkArchiveResult {
export interface CreateDocumentPayload {
asset_id: string;
type_code: string;
number: string;
/** № документа is optional (issue #18) — backend column is nullable. */
number: string | null;
issue_date: string | null;
expiry_date: string | null;
responsible_user_id: string | null;
Expand Down
16 changes: 7 additions & 9 deletions web/src/pages/registry/DocumentCreateDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ import { QuickTypeDialog } from "./QuickTypeDialog";
const createDocSchema = z.object({
asset_id: z.string().min(1, "Выберите компанию"),
type_code: z.string().min(1, "Выберите тип документа"),
number: z.string().min(1, "Обязательное поле"),
number: z.string().max(500, "Максимум 500 символов").nullable().optional(),
issue_date: z.string().nullable().optional(),
expiry_date: z.string().nullable().optional(),
notes: z.string().max(10000, "Максимум 10 000 символов").nullable().optional(),
Expand Down Expand Up @@ -125,7 +125,9 @@ export function DocumentCreateDialog({ open, onClose }: DocumentCreateDialogProp
await createMutation.mutateAsync({
asset_id: values.asset_id,
type_code: values.type_code,
number: values.number,
// № документа is optional (backend column is nullable) — send null for an
// empty/whitespace value so the document is created without a number.
number: values.number?.trim() ? values.number.trim() : null,
issue_date: values.issue_date ?? null,
expiry_date: values.expiry_date ?? null,
responsible_user_id: claims?.sub ?? null,
Expand Down Expand Up @@ -273,13 +275,9 @@ export function DocumentCreateDialog({ open, onClose }: DocumentCreateDialogProp
</div>
)}

{/* № документа */}
<FormField label="№ документа *" error={errors.number?.message}>
<Input
{...register("number")}
placeholder="Например: ДГ-2026-001"
aria-required="true"
/>
{/* № документа — optional (issue #18) */}
<FormField label="№ документа" error={errors.number?.message}>
<Input {...register("number")} placeholder="Например: ДГ-2026-001 (необязательно)" />
</FormField>

{/* Dates */}
Expand Down
2 changes: 1 addition & 1 deletion web/src/pages/registry/RegistryPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -521,7 +521,7 @@ export function RegistryPage() {
header: effectiveLabel("number", t("registry.col_number")),
cell: (info) => (
<span className="font-mono text-xs" data-analytics-key="number">
{info.getValue()}
{info.getValue() || "—"}
</span>
),
enableSorting: true,
Expand Down
33 changes: 17 additions & 16 deletions web/src/pages/registry/__tests__/DocumentCreateDialog.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
* Unit tests for DocumentCreateDialog (US-5, US-7).
*
* Business rules under test:
* - Submit button is disabled until asset_id, type_code and number are filled
* - Submit button is disabled until asset_id and type_code are filled
* (№ документа is optional — issue #18; blank number submits as null)
* - Notes field accepts up to 10 000 chars (schema boundary)
* - useCreateDocument.mutate is called with the correct payload on valid submit
* - Escape key resets the form and calls onClose
Expand Down Expand Up @@ -143,33 +144,33 @@ describe("DocumentCreateDialog — required fields gate", () => {
});
});

it("test_number_field_required_error_shown_when_blank_submitted", async () => {
it("test_number_optional_blank_submits_with_null", async () => {
// issue #18: № документа is optional. With asset + type filled but number
// left blank, the form is valid and submits with number = null.
const user = userEvent.setup();
await renderDialog();

await waitFor(() => {
expect(screen.getByRole("combobox", { name: /Компания/i })).toBeTruthy();
});

// Fill asset + type but leave number blank
const assetSelect = screen.getByRole("combobox", { name: /Компания/i });
const typeSelect = screen.getByRole("combobox", { name: /Тип документа/i });

await user.selectOptions(assetSelect, "a-001");
await user.selectOptions(typeSelect, "contract");

// Click submit without filling number
const submitBtn = screen.queryByRole("button", { name: /Создать документ/i });
if (submitBtn && !submitBtn.hasAttribute("disabled")) {
await user.click(submitBtn);
await waitFor(() => {
// Expect either a field error or that mutate was NOT called
expect(mockMutateAsync).not.toHaveBeenCalled();
});
} else {
// Submit already disabled — correct behavior
expect(submitBtn?.hasAttribute("disabled") ?? true).toBe(true);
}
// Submit is enabled even though the number is blank (no required-field gate).
const submitBtn = screen.getByRole("button", { name: /Создать документ/i });
expect(submitBtn).not.toBeDisabled();
await user.click(submitBtn);

await waitFor(() => {
expect(mockMutateAsync).toHaveBeenCalled();
});
const payload = mockMutateAsync.mock.calls[0]?.[0] as Record<string, unknown>;
expect(payload?.asset_id).toBe("a-001");
expect(payload?.type_code).toBe("contract");
expect(payload?.number).toBeNull();
});
});

Expand Down
Loading