chore(repo): дедуп formatAmount, пины образов, дефолты MinIO, доля any

Шаги 2.3 и 2.4 плана production-готовности:
- lib/format.ts formatAmount вместо 2 локальных копий
- пины тегов minio/rclone/ollama/traccar/pgvector + osrm по digest
- minioadmin без дефолта (env required, dev-подсказки в .env.example)
- 28 any в auto-transitions/shared типизированы из @shared/schema
- удалён _tablefield_orig.txt
- check/lint/build чисто, vitest 104/104
This commit is contained in:
2026-09-08 01:07:55 +03:00
parent 5750a3106f
commit 75ec0f25bb
12 changed files with 118 additions and 204 deletions

View File

@@ -8,6 +8,10 @@
*/
import { storage } from "../storage";
import type { FormField, FormStatus, SafeUser, StatusTransition, Task } from "@shared/schema";
// Тип роли с метаданными, как её возвращает storage.getRoles
type OrgRole = Awaited<ReturnType<typeof storage.getRoles>>[number];
import { fieldConditionsMatch, resolveFieldDisplayText, type FieldConditionContext, SYSTEM_TASK_FIELDS, buildSystemFieldValuesForConditions } from "./field-conditions";
import { resolveTaskFieldTitles, buildTableRowMap, formatFieldValueForTitle, eventBus } from "../routes/shared";
@@ -22,11 +26,11 @@ export interface EvaluateAutoTransitionsOptions {
triggeredBy?: number | null;
maxDepth?: number;
preloaded?: {
statuses?: any[];
formFields?: any[];
users?: any[];
roles?: any[];
transitions?: any[];
statuses?: FormStatus[];
formFields?: FormField[];
users?: SafeUser[];
roles?: OrgRole[];
transitions?: StatusTransition[];
tableMap?: Map<number, { rows: Array<{ id: number; values: string[] }>; columns: Array<{ name: string }> }>;
};
}
@@ -55,7 +59,7 @@ export async function evaluateAutoTransitions(
if (!_task) {
return { changed: false, task: { id: taskId, currentStatusId: 0, title: "", formId: 0, assignedTo: null }, visitedStatusIds: [] };
}
let task: any = _task;
let task: Task = _task;
const formId = task.formId;
const [statuses, formFields, fieldValues] = await Promise.all([
@@ -65,7 +69,7 @@ export async function evaluateAutoTransitions(
]);
// Merge system fields so rules can reference _assignee, _status, _due_date, etc.
const currentStatusName = statuses.find((s: any) => s.id === task.currentStatusId)?.name;
const currentStatusName = statuses.find((s) => s.id === task.currentStatusId)?.name;
const systemFieldValues = buildSystemFieldValuesForConditions(task, { statusName: currentStatusName });
const allFields = [...formFields, ...SYSTEM_TASK_FIELDS];
let allFieldValues = [...fieldValues, ...systemFieldValues];
@@ -74,27 +78,26 @@ export async function evaluateAutoTransitions(
const orgUsers = options.preloaded?.users ? options.preloaded.users : await storage.getUsersByOrganization(organizationId);
const orgRoles = options.preloaded?.roles ? options.preloaded.roles : await storage.getRoles(organizationId);
const usersWithFullName = orgUsers.map((u: any) => ({
const usersWithFullName = orgUsers.map((u) => ({
...u,
fullName: `${u.firstName || ""} ${u.middleName || ""} ${u.lastName || ""}`.trim() || u.email,
}));
const valueByFieldId = new Map(fieldValues.map((v: any) => [v.fieldId, v.value]));
const valueByFieldId = new Map(fieldValues.map((v) => [v.fieldId, v.value]));
const taskTitleMap = await resolveTaskFieldTitles(valueByFieldId, formFields, organizationId);
const tableIds = new Set<number>();
for (const f of formFields) {
if ((f as any).type === "table") {
const tid = ((f as any).options as Record<string, number> | undefined)?.tableId;
if (f.type === "table") {
const tid = (f.options as { tableId?: number } | null)?.tableId;
if (tid) tableIds.add(tid);
}
}
let tableMap = options.preloaded?.tableMap;
if (!tableMap) {
tableMap = new Map<number, { rows: Array<{ id: number; values: string[] }>; columns: Array<{ name: string }> }>();
const _storage = storage as any;
for (const tableId of tableIds) {
const fullTable = await _storage.getDataTableFull(tableId, organizationId);
const fullTable = await storage.getDataTableFull(tableId, organizationId);
if (fullTable) {
tableMap.set(tableId, {
rows: fullTable.rows as Array<{ id: number; values: string[] }>,
@@ -153,7 +156,7 @@ export async function evaluateAutoTransitions(
task = await applyAutoTransition(task, targetStatusId, statuses, allFields, allFieldValues, conditionContext, organizationId, transitions, options.triggeredBy);
// Refresh system field values after transition so chained transitions evaluate against updated state
const newStatusName = statuses.find((s: any) => s.id === task.currentStatusId)?.name;
const newStatusName = statuses.find((s) => s.id === task.currentStatusId)?.name;
const newSystemFieldValues = buildSystemFieldValuesForConditions(task, { statusName: newStatusName });
allFieldValues = [...fieldValues, ...newSystemFieldValues];
}
@@ -170,16 +173,24 @@ export async function evaluateAutoTransitions(
* Применяет один автопереход с учётом side effects.
*/
async function applyAutoTransition(
task: any,
task: Task,
targetStatusId: number,
statuses: any[],
formFields: any[],
fieldValues: any[],
statuses: FormStatus[],
// Поля формы + системные поля (_assignee, _status и т.п.) — структурный тип как в fieldConditionsMatch
formFields: Array<{
id: number;
code: string;
type?: string;
options?: unknown;
autoProlongationLabel?: string | null;
}>,
// Значения полей + системные значения — структурный тип как в fieldConditionsMatch
fieldValues: Array<{ fieldId: number; value: unknown }>,
conditionContext: FieldConditionContext,
organizationId: number,
transitions: any[],
transitions: StatusTransition[],
triggeredBy: number | null = null
): Promise<any> {
): Promise<Task> {
const fromStatus = statuses.find((s) => s.id === task.currentStatusId);
const toStatus = statuses.find((s) => s.id === targetStatusId);
const isFinalStatus = toStatus?.isFinal || false;
@@ -187,7 +198,7 @@ async function applyAutoTransition(
// Determine assignee from transitions (if any transition from current → target has assignee rules)
let targetAssigneeId: number | null = task.assignedTo;
const matchingTransitions = transitions.filter(
(t: any) => t.fromStatusId === task.currentStatusId && t.toStatusId === targetStatusId
(t) => t.fromStatusId === task.currentStatusId && t.toStatusId === targetStatusId
);
for (const transition of matchingTransitions) {
@@ -209,7 +220,7 @@ async function applyAutoTransition(
}
if (assignee.type === "role") {
const members = await storage.getRoleMembersByRole(assignee.id);
const firstUser = members.find((m: any) => m.userId);
const firstUser = members.find((m) => m.userId);
if (firstUser) {
targetAssigneeId = firstUser.userId;
break;
@@ -223,7 +234,7 @@ async function applyAutoTransition(
userId = task.createdBy ?? undefined;
} else {
const field = formFields.find(f => f.code === assignee.fieldCode);
const fieldValue = fieldValues.find((fv: any) => fv.fieldId === field?.id);
const fieldValue = fieldValues.find((fv) => fv.fieldId === field?.id);
userId = fieldValue?.value ? parseInt(String(fieldValue.value)) : undefined;
}
if (userId && userId > 0) {
@@ -233,10 +244,10 @@ async function applyAutoTransition(
}
if (assignee.type === 'table_column_user') {
const field = formFields.find(f => f.code === assignee.fieldCode);
const fieldValue = fieldValues.find((fv: any) => fv.fieldId === field?.id);
const fieldValue = fieldValues.find((fv) => fv.fieldId === field?.id);
const rowId = fieldValue?.value ? parseInt(String(fieldValue.value)) : NaN;
if (!isNaN(rowId) && rowId > 0) {
const tableId = (field as any)?.options?.tableId as number;
const tableId = (field?.options as { tableId?: number } | null)?.tableId;
if (tableId) {
const row = await storage.getDataTableRow(rowId, tableId, organizationId);
if (row) {
@@ -337,10 +348,10 @@ async function applyAutoTransition(
storage.getRoles(organizationId),
]);
const { buildSystemFieldValues } = await import("../routes/shared.js");
const fieldCodeMap = new Map(allFormFields.map((f: any) => [f.code, f]));
const valueByFieldId = new Map(savedValues.map((v: any) => [v.fieldId, v.value]));
const usersMap = new Map(titleUsers.map((u: any) => [u.id, u]));
const rolesMap = new Map(titleRoles.map((r: any) => [r.id, r]));
const fieldCodeMap = new Map(allFormFields.map((f) => [f.code, f]));
const valueByFieldId = new Map(savedValues.map((v) => [v.fieldId, v.value]));
const usersMap = new Map(titleUsers.map((u) => [u.id, u]));
const rolesMap = new Map(titleRoles.map((r) => [r.id, r]));
const sysValues = await buildSystemFieldValues(updatedTask, organizationId, { statuses: titleStatuses, usersMap });
const titleTaskTitleMap = await resolveTaskFieldTitles(valueByFieldId, allFormFields, organizationId);
const tableRowMap = await buildTableRowMap(allFormFields, organizationId);