fix(number-fields): избегаем потери точности длинных чисел
- number-поля теперь рендерятся как text + inputMode=numeric, чтобы браузер не округлял значения через input type=number - пробелы при вставке в number-поля удаляются - бэкенд нормализует значения number-полей в строку перед сохранением - добавлен хелпер normalizeFieldValueForStorage Closes: искажение расчётного счёта и других длинных числовых полей
This commit is contained in:
481
server/storage/tasks-core.storage.ts
Normal file
481
server/storage/tasks-core.storage.ts
Normal file
@@ -0,0 +1,481 @@
|
||||
import { users, forms, formStatuses, tasks, taskFieldValues, fieldHistory, type User, type Form, type FormStatus, type Task, type TaskFieldValue, type FieldHistory, type InsertTask, type InsertTaskFieldValue, type InsertFieldHistory } from "@shared/schema";
|
||||
import { taskRelations } from "@shared/schema";
|
||||
import { taskAssignees } from "@shared/schema";
|
||||
import { db } from "../db";
|
||||
import { eq, and, or, desc, asc, exists, sql, ilike, inArray } from "drizzle-orm";
|
||||
import { formatUserName } from "../utils/formatUserName";
|
||||
import { FormsStorage } from "./forms.storage";
|
||||
export class TasksCoreStorage extends FormsStorage {
|
||||
// Tasks
|
||||
async getTask(id: number, organizationId: number): Promise<Task | undefined> {
|
||||
const results = await db
|
||||
.select()
|
||||
.from(tasks)
|
||||
.innerJoin(forms, eq(tasks.formId, forms.id))
|
||||
.where(and(eq(tasks.id, id), eq(forms.organizationId, organizationId)));
|
||||
const [result] = results;
|
||||
return result?.tasks || undefined;
|
||||
}
|
||||
|
||||
async getTasksByForm(formId: number, organizationId: number): Promise<Task[]> {
|
||||
return db
|
||||
.select()
|
||||
.from(tasks)
|
||||
.innerJoin(forms, eq(tasks.formId, forms.id))
|
||||
.where(and(eq(tasks.formId, formId), eq(forms.organizationId, organizationId)))
|
||||
.orderBy(desc(tasks.createdAt))
|
||||
.then(results => results.map(r => r.tasks));
|
||||
}
|
||||
|
||||
async getTasksWithFieldsByForm(formId: number, organizationId: number): Promise<Task[]> {
|
||||
const allTasks = await db
|
||||
.select()
|
||||
.from(tasks)
|
||||
.innerJoin(forms, eq(tasks.formId, forms.id))
|
||||
.where(and(eq(tasks.formId, formId), eq(forms.organizationId, organizationId)))
|
||||
.orderBy(desc(tasks.createdAt))
|
||||
.then(results => results.map(r => r.tasks));
|
||||
|
||||
if (allTasks.length === 0) return [];
|
||||
|
||||
const taskIds = allTasks.map(t => t.id);
|
||||
const allFieldValues = await db
|
||||
.select()
|
||||
.from(taskFieldValues)
|
||||
.where(inArray(taskFieldValues.taskId, taskIds));
|
||||
|
||||
const fieldValuesByTask = new Map<number, { [fieldId: number]: unknown }>();
|
||||
for (const fv of allFieldValues) {
|
||||
if (!fieldValuesByTask.has(fv.taskId)) {
|
||||
fieldValuesByTask.set(fv.taskId, {});
|
||||
}
|
||||
fieldValuesByTask.get(fv.taskId)![fv.fieldId] = fv.value;
|
||||
}
|
||||
|
||||
return allTasks.map(task => ({
|
||||
...task,
|
||||
fieldValues: fieldValuesByTask.get(task.id) || {}
|
||||
}));
|
||||
}
|
||||
|
||||
async getTasksWithFieldsByFormOptimized(
|
||||
formId: number,
|
||||
organizationId: number,
|
||||
options?: { limit?: number; offset?: number; assignedTo?: number }
|
||||
): Promise<{ tasks: Task[]; total: number }> {
|
||||
const limit = options?.limit;
|
||||
const offset = options?.offset || 0;
|
||||
|
||||
const whereConditions = [
|
||||
eq(tasks.formId, formId),
|
||||
eq(forms.organizationId, organizationId),
|
||||
];
|
||||
if (options?.assignedTo != null) {
|
||||
whereConditions.push(eq(tasks.assignedTo, options.assignedTo));
|
||||
}
|
||||
|
||||
let query = db
|
||||
.select()
|
||||
.from(tasks)
|
||||
.innerJoin(forms, eq(tasks.formId, forms.id))
|
||||
.where(and(...whereConditions))
|
||||
.orderBy(desc(tasks.updatedAt))
|
||||
.offset(offset);
|
||||
|
||||
const allTasks = limit
|
||||
? await query.limit(limit).then(results => results.map(r => r.tasks))
|
||||
: await query.then(results => results.map(r => r.tasks));
|
||||
|
||||
if (allTasks.length === 0) {
|
||||
return { tasks: [], total: 0 };
|
||||
}
|
||||
|
||||
const countResult = await db
|
||||
.select({ count: sql<number>`count(*)` })
|
||||
.from(tasks)
|
||||
.innerJoin(forms, eq(tasks.formId, forms.id))
|
||||
.where(and(...whereConditions));
|
||||
const total = Number(countResult[0]?.count || 0);
|
||||
|
||||
const taskIds = allTasks.map(t => t.id);
|
||||
const [allFieldValues, allTaskAssignees] = await Promise.all([
|
||||
db.select().from(taskFieldValues).where(inArray(taskFieldValues.taskId, taskIds)),
|
||||
db
|
||||
.select({
|
||||
taskId: taskAssignees.taskId,
|
||||
userId: taskAssignees.userId,
|
||||
firstName: users.firstName,
|
||||
middleName: users.middleName,
|
||||
lastName: users.lastName,
|
||||
email: users.email,
|
||||
})
|
||||
.from(taskAssignees)
|
||||
.innerJoin(users, eq(taskAssignees.userId, users.id))
|
||||
.where(inArray(taskAssignees.taskId, taskIds)),
|
||||
]);
|
||||
|
||||
const fieldValuesByTask = new Map<number, { [fieldId: number]: unknown }>();
|
||||
for (const fv of allFieldValues) {
|
||||
if (!fieldValuesByTask.has(fv.taskId)) {
|
||||
fieldValuesByTask.set(fv.taskId, {});
|
||||
}
|
||||
fieldValuesByTask.get(fv.taskId)![fv.fieldId] = fv.value;
|
||||
}
|
||||
|
||||
const assigneesByTask = new Map<number, Array<{ userId: number; user: { firstName: string | null; middleName: string | null; lastName: string | null; email: string } }>>();
|
||||
for (const a of allTaskAssignees) {
|
||||
if (!assigneesByTask.has(a.taskId)) {
|
||||
assigneesByTask.set(a.taskId, []);
|
||||
}
|
||||
assigneesByTask.get(a.taskId)!.push({ userId: a.userId, user: { firstName: a.firstName, middleName: a.middleName, lastName: a.lastName, email: a.email } });
|
||||
}
|
||||
|
||||
const tasksWithFields = allTasks.map(task => ({
|
||||
...task,
|
||||
fieldValues: fieldValuesByTask.get(task.id) || {},
|
||||
assignees: assigneesByTask.get(task.id) || [],
|
||||
}));
|
||||
|
||||
return { tasks: tasksWithFields, total };
|
||||
}
|
||||
|
||||
async getTaskCountsByStatus(formId: number, organizationId: number): Promise<Array<{ statusId: number; name: string; color: string; count: number }>> {
|
||||
const statuses = await db
|
||||
.select()
|
||||
.from(formStatuses)
|
||||
.where(and(eq(formStatuses.formId, formId), eq(formStatuses.isFinal, false)))
|
||||
.orderBy(asc(formStatuses.position));
|
||||
|
||||
if (statuses.length === 0) return [];
|
||||
|
||||
const countsResult = await db
|
||||
.select({
|
||||
currentStatusId: tasks.currentStatusId,
|
||||
count: sql<number>`count(*)::int`,
|
||||
})
|
||||
.from(tasks)
|
||||
.innerJoin(forms, eq(tasks.formId, forms.id))
|
||||
.where(and(eq(tasks.formId, formId), eq(forms.organizationId, organizationId)))
|
||||
.groupBy(tasks.currentStatusId);
|
||||
|
||||
const countMap = new Map(countsResult.map(c => [c.currentStatusId, c.count]));
|
||||
|
||||
return statuses.map(s => ({
|
||||
statusId: s.id,
|
||||
name: s.name,
|
||||
color: s.color || '#94a3b8',
|
||||
count: countMap.get(s.id) || 0,
|
||||
}));
|
||||
}
|
||||
|
||||
async getTasksByOrganization(
|
||||
organizationId: number,
|
||||
options?: {
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
sortBy?: 'updatedAt' | 'createdAt';
|
||||
order?: 'asc' | 'desc';
|
||||
statusId?: number;
|
||||
assignedTo?: number;
|
||||
minimal?: boolean;
|
||||
}
|
||||
): Promise<Task[] | Partial<Task>[]> {
|
||||
const sortBy = options?.sortBy || 'updatedAt';
|
||||
const order = options?.order || 'desc';
|
||||
const limit = options?.limit;
|
||||
const offset = options?.offset;
|
||||
const statusId = options?.statusId;
|
||||
const assignedTo = options?.assignedTo;
|
||||
const minimal = options?.minimal || false;
|
||||
|
||||
const sortColumn = sortBy === 'updatedAt' ? tasks.updatedAt : tasks.createdAt;
|
||||
const orderFn = order === 'desc' ? desc : asc;
|
||||
|
||||
const conditions = [eq(forms.organizationId, organizationId)];
|
||||
|
||||
if (statusId) {
|
||||
conditions.push(eq(tasks.currentStatusId, statusId));
|
||||
}
|
||||
if (assignedTo) {
|
||||
conditions.push(eq(tasks.assignedTo, assignedTo));
|
||||
}
|
||||
|
||||
if (minimal) {
|
||||
return await db
|
||||
.select({
|
||||
id: tasks.id,
|
||||
formId: tasks.formId,
|
||||
title: tasks.title,
|
||||
currentStatusId: tasks.currentStatusId,
|
||||
assignedTo: tasks.assignedTo,
|
||||
updatedAt: tasks.updatedAt,
|
||||
createdAt: tasks.createdAt,
|
||||
status: formStatuses.name
|
||||
})
|
||||
.from(tasks)
|
||||
.innerJoin(forms, eq(tasks.formId, forms.id))
|
||||
.leftJoin(formStatuses, eq(tasks.currentStatusId, formStatuses.id))
|
||||
.where(and(...conditions))
|
||||
.orderBy(orderFn(sortColumn))
|
||||
.limit(limit || 1000)
|
||||
.offset(offset || 0);
|
||||
}
|
||||
|
||||
const results = await db
|
||||
.select()
|
||||
.from(tasks)
|
||||
.innerJoin(forms, eq(tasks.formId, forms.id))
|
||||
.where(and(...conditions))
|
||||
.orderBy(orderFn(sortColumn))
|
||||
.limit(limit || 10000)
|
||||
.offset(offset || 0);
|
||||
|
||||
return results.map((r) => r.tasks);
|
||||
}
|
||||
|
||||
async createTask(insertTask: InsertTask): Promise<Task> {
|
||||
let depth = 0;
|
||||
let position = 0;
|
||||
|
||||
if (insertTask.parentTaskId) {
|
||||
const [parentTask] = await db
|
||||
.select()
|
||||
.from(tasks)
|
||||
.where(eq(tasks.id, insertTask.parentTaskId));
|
||||
|
||||
if (parentTask) {
|
||||
depth = (parentTask.depth || 0) + 1;
|
||||
|
||||
const siblings = await db
|
||||
.select({ count: sql<number>`count(*)` })
|
||||
.from(tasks)
|
||||
.where(eq(tasks.parentTaskId, insertTask.parentTaskId));
|
||||
|
||||
position = Number(siblings[0]?.count || 0);
|
||||
}
|
||||
}
|
||||
|
||||
const [task] = await db
|
||||
.insert(tasks)
|
||||
.values({
|
||||
...(insertTask as typeof tasks.$inferInsert),
|
||||
depth,
|
||||
position,
|
||||
})
|
||||
.returning();
|
||||
return task;
|
||||
}
|
||||
|
||||
async getSubtasks(parentTaskId: number, organizationId: number): Promise<Task[]> {
|
||||
return await db
|
||||
.select()
|
||||
.from(tasks)
|
||||
.innerJoin(forms, eq(tasks.formId, forms.id))
|
||||
.where(and(
|
||||
eq(tasks.parentTaskId, parentTaskId),
|
||||
eq(forms.organizationId, organizationId)
|
||||
))
|
||||
.orderBy(tasks.position)
|
||||
.then(results => results.map(r => r.tasks));
|
||||
}
|
||||
|
||||
async getTaskTree(taskId: number, organizationId: number): Promise<any> {
|
||||
const task = await this.getTask(taskId, organizationId);
|
||||
if (!task) return null;
|
||||
|
||||
const subtasks = await this.getSubtasks(taskId, organizationId);
|
||||
const subtasksWithChildren = await Promise.all(
|
||||
subtasks.map(async (subtask) => {
|
||||
return await this.getTaskTree(subtask.id, organizationId);
|
||||
})
|
||||
);
|
||||
|
||||
return {
|
||||
...task,
|
||||
subtasks: subtasksWithChildren.filter(Boolean)
|
||||
};
|
||||
}
|
||||
|
||||
async updateTask(id: number, organizationId: number, updates: Partial<Task>): Promise<Task> {
|
||||
const [task] = await db
|
||||
.update(tasks)
|
||||
.set({ ...updates, updatedAt: new Date() })
|
||||
.where(and(
|
||||
eq(tasks.id, id),
|
||||
exists(db.select().from(forms).where(and(eq(forms.id, tasks.formId), eq(forms.organizationId, organizationId))))
|
||||
))
|
||||
.returning();
|
||||
return task;
|
||||
}
|
||||
|
||||
async updateTaskIfNotModified(
|
||||
id: number,
|
||||
organizationId: number,
|
||||
updates: Partial<Task>,
|
||||
requiredUpdatedAt: Date,
|
||||
): Promise<Task | null> {
|
||||
const [task] = await db
|
||||
.update(tasks)
|
||||
.set({ ...updates, updatedAt: new Date() })
|
||||
.where(and(
|
||||
eq(tasks.id, id),
|
||||
eq(tasks.updatedAt, requiredUpdatedAt),
|
||||
exists(db.select().from(forms).where(and(eq(forms.id, tasks.formId), eq(forms.organizationId, organizationId))))
|
||||
))
|
||||
.returning();
|
||||
return task ?? null;
|
||||
}
|
||||
|
||||
async deleteTask(id: number, organizationId: number): Promise<void> {
|
||||
// Cleanup RAG embeddings for this task and its messages
|
||||
const { deleteRagEmbeddingsByTask } = await import('../services/embedding.service.js');
|
||||
await deleteRagEmbeddingsByTask(organizationId, id);
|
||||
|
||||
await db
|
||||
.delete(tasks)
|
||||
.where(and(
|
||||
eq(tasks.id, id),
|
||||
exists(
|
||||
db.select()
|
||||
.from(forms)
|
||||
.where(and(eq(forms.id, tasks.formId), eq(forms.organizationId, organizationId)))
|
||||
)
|
||||
));
|
||||
}
|
||||
|
||||
async searchTasksByKeyword(
|
||||
organizationId: number,
|
||||
query: string,
|
||||
limit: number = 10
|
||||
): Promise<Pick<Task, 'id' | 'title' | 'description' | 'formId' | 'currentStatusId'>[]> {
|
||||
const pattern = `%${query}%`;
|
||||
const results = await db
|
||||
.select({
|
||||
id: tasks.id,
|
||||
title: tasks.title,
|
||||
description: tasks.description,
|
||||
formId: tasks.formId,
|
||||
currentStatusId: tasks.currentStatusId,
|
||||
})
|
||||
.from(tasks)
|
||||
.innerJoin(forms, eq(tasks.formId, forms.id))
|
||||
.where(
|
||||
and(
|
||||
eq(forms.organizationId, organizationId),
|
||||
or(
|
||||
ilike(tasks.title, pattern),
|
||||
ilike(tasks.description, pattern)
|
||||
)
|
||||
)
|
||||
)
|
||||
.orderBy(desc(tasks.updatedAt))
|
||||
.limit(limit);
|
||||
return results;
|
||||
}
|
||||
|
||||
// Task Field Values
|
||||
async getTaskFieldValues(taskId: number, organizationId: number): Promise<TaskFieldValue[]> {
|
||||
return await db
|
||||
.select()
|
||||
.from(taskFieldValues)
|
||||
.innerJoin(tasks, eq(taskFieldValues.taskId, tasks.id))
|
||||
.innerJoin(forms, eq(tasks.formId, forms.id))
|
||||
.where(and(eq(taskFieldValues.taskId, taskId), eq(forms.organizationId, organizationId)))
|
||||
.orderBy(taskFieldValues.fieldId)
|
||||
.then(results => results.map(r => r.task_field_values));
|
||||
}
|
||||
|
||||
async createTaskFieldValue(insertValue: InsertTaskFieldValue): Promise<TaskFieldValue> {
|
||||
const [value] = await db
|
||||
.insert(taskFieldValues)
|
||||
.values(insertValue)
|
||||
.returning();
|
||||
return value;
|
||||
}
|
||||
|
||||
async updateTaskFieldValue(taskId: number, fieldId: number, organizationId: number, updates: Partial<TaskFieldValue>): Promise<TaskFieldValue> {
|
||||
const [value] = await db
|
||||
.update(taskFieldValues)
|
||||
.set({ ...updates, updatedAt: new Date() })
|
||||
.where(and(
|
||||
eq(taskFieldValues.taskId, taskId),
|
||||
eq(taskFieldValues.fieldId, fieldId),
|
||||
exists(
|
||||
db.select()
|
||||
.from(tasks)
|
||||
.innerJoin(forms, eq(tasks.formId, forms.id))
|
||||
.where(and(eq(tasks.id, taskId), eq(forms.organizationId, organizationId)))
|
||||
)
|
||||
))
|
||||
.returning();
|
||||
return value;
|
||||
}
|
||||
|
||||
async bulkUpsertTaskFieldValues(taskId: number, formId: number, updates: Array<{ fieldId: number; value: unknown }>): Promise<TaskFieldValue[]> {
|
||||
const results: TaskFieldValue[] = [];
|
||||
await db.transaction(async (tx) => {
|
||||
const existing = await tx
|
||||
.select()
|
||||
.from(taskFieldValues)
|
||||
.where(eq(taskFieldValues.taskId, taskId));
|
||||
const existingMap = new Map(existing.map(v => [v.fieldId, v]));
|
||||
for (const { fieldId, value } of updates) {
|
||||
const existingValue = existingMap.get(fieldId);
|
||||
if (existingValue) {
|
||||
const [updated] = await tx
|
||||
.update(taskFieldValues)
|
||||
.set({ value, updatedAt: new Date() })
|
||||
.where(and(eq(taskFieldValues.taskId, taskId), eq(taskFieldValues.fieldId, fieldId)))
|
||||
.returning();
|
||||
results.push(updated);
|
||||
} else {
|
||||
const [inserted] = await tx
|
||||
.insert(taskFieldValues)
|
||||
.values({ taskId, fieldId, formId, value })
|
||||
.returning();
|
||||
results.push(inserted);
|
||||
}
|
||||
}
|
||||
});
|
||||
return results;
|
||||
}
|
||||
|
||||
async deleteTaskFieldValue(taskId: number, fieldId: number, organizationId: number): Promise<void> {
|
||||
await db
|
||||
.delete(taskFieldValues)
|
||||
.where(and(
|
||||
eq(taskFieldValues.taskId, taskId),
|
||||
eq(taskFieldValues.fieldId, fieldId),
|
||||
exists(
|
||||
db.select()
|
||||
.from(tasks)
|
||||
.innerJoin(forms, eq(tasks.formId, forms.id))
|
||||
.where(and(eq(tasks.id, taskId), eq(forms.organizationId, organizationId)))
|
||||
)
|
||||
));
|
||||
}
|
||||
|
||||
// Field History
|
||||
async getFieldHistory(taskId: number, fieldId: number, organizationId: number): Promise<FieldHistory[]> {
|
||||
return await db
|
||||
.select()
|
||||
.from(fieldHistory)
|
||||
.innerJoin(tasks, eq(fieldHistory.taskId, tasks.id))
|
||||
.innerJoin(forms, eq(tasks.formId, forms.id))
|
||||
.where(and(
|
||||
eq(fieldHistory.taskId, taskId),
|
||||
eq(fieldHistory.fieldId, fieldId),
|
||||
eq(forms.organizationId, organizationId)
|
||||
))
|
||||
.orderBy(desc(fieldHistory.changedAt))
|
||||
.then(results => results.map(r => r.field_history));
|
||||
}
|
||||
|
||||
async createFieldHistory(insertHistory: InsertFieldHistory): Promise<FieldHistory> {
|
||||
const [history] = await db
|
||||
.insert(fieldHistory)
|
||||
.values(insertHistory)
|
||||
.returning();
|
||||
return history;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user