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:
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user