18c2959820
New "Импорт работ из Excel" section checks the planned volumes file against /Import/StroyBot/Works as soon as it is picked. The builder turns checked rows into works and decides per work whether to create it or update the volume and unit of an existing work with the same title on the object. Saving comes next. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
111 lines
5.4 KiB
TypeScript
111 lines
5.4 KiB
TypeScript
import { PageId } from '../routing/routes';
|
|
import { IAccessibleProject, IAccessRow, ProjectStatusKind } from './projectTypes';
|
|
|
|
// Правила главной страницы (stroy-bot-main), вынесенные из DOM-кода в чистые функции.
|
|
|
|
/** Сколько проектов показывать в выпадающем списке и сколько «недавних» поднимать наверх. */
|
|
export const PROJECT_LIST_LIMIT = 7;
|
|
|
|
/** Несколько записей «Доступы» на один проект сливаются: canEdit — OR по edit0. */
|
|
export function mergeAccessRows(rows: IAccessRow[]): IAccessibleProject[] {
|
|
const byId = new Map<number, IAccessibleProject>();
|
|
rows.forEach((row) => {
|
|
if (!row.project) {
|
|
return;
|
|
}
|
|
const existing = byId.get(row.project.Id);
|
|
if (existing) {
|
|
existing.canEdit = existing.canEdit || !!row.edit0;
|
|
return;
|
|
}
|
|
byId.set(row.project.Id, { id: row.project.Id, title: row.project.Title ?? '', canEdit: !!row.edit0 });
|
|
});
|
|
|
|
const projects: IAccessibleProject[] = [];
|
|
byId.forEach((project) => projects.push(project));
|
|
return projects;
|
|
}
|
|
|
|
/** Алфавитный порядок, затем недавно активные (в порядке recentIds) — наверх. */
|
|
export function sortProjects(projects: IAccessibleProject[], recentIds: number[]): IAccessibleProject[] {
|
|
const alphabetical = [...projects].sort((a, b) => a.title.localeCompare(b.title));
|
|
if (recentIds.length === 0) {
|
|
return alphabetical;
|
|
}
|
|
return alphabetical.sort((a, b) => {
|
|
const aIndex = recentIds.indexOf(a.id);
|
|
const bIndex = recentIds.indexOf(b.id);
|
|
if (aIndex !== -1 && bIndex !== -1) return aIndex - bIndex;
|
|
if (aIndex !== -1) return -1;
|
|
if (bIndex !== -1) return 1;
|
|
return 0;
|
|
});
|
|
}
|
|
|
|
/** Первые N уникальных projectId из отчётов, уже отсортированных по дате (desc). */
|
|
export function pickRecentProjectIds(projectIds: Array<number | undefined>): number[] {
|
|
const result: number[] = [];
|
|
for (const id of projectIds) {
|
|
if (id !== undefined && result.indexOf(id) === -1) {
|
|
result.push(id);
|
|
if (result.length === PROJECT_LIST_LIMIT) break;
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
/** Поиск в выпадающем списке: подстрока без учёта регистра, не больше 7 строк. */
|
|
export function filterProjects(projects: IAccessibleProject[], search: string): IAccessibleProject[] {
|
|
const query = search.toLowerCase();
|
|
return projects.filter((project) => project.title.toLowerCase().indexOf(query) !== -1).slice(0, PROJECT_LIST_LIMIT);
|
|
}
|
|
|
|
export function getStatusKind(status: string | undefined): ProjectStatusKind {
|
|
const value = status?.trim().toLowerCase() ?? '';
|
|
if (value === 'текущий') return 'current';
|
|
if (value === 'завершен' || value === 'завершён') return 'done';
|
|
if (value === 'архивный') return 'archive';
|
|
return 'unknown';
|
|
}
|
|
|
|
/**
|
|
* Доступен ли раздел для проекта. Правила карточек stroy-bot-main:
|
|
* - архивный проект — закрыто всё (кроме архива, он не раздел приложения);
|
|
* - завершён — нельзя «Отправить отчёт»;
|
|
* - нет права edit0 — нельзя «Отправить отчёт».
|
|
* Импорт отчёта из Excel создаёт отчёты — те же правила, что у «Отправить отчёт».
|
|
* Привязка к справочнику — только с правом редактирования (решение при объединении).
|
|
* Импорт работ из Excel меняет работы проекта — тоже только с правом редактирования.
|
|
*/
|
|
export function isSectionAvailable(page: PageId, project: IAccessibleProject): boolean {
|
|
if (page === 'hub') return true;
|
|
|
|
const kind = getStatusKind(project.status);
|
|
if (kind === 'archive') return false;
|
|
if (page === 'report' || page === 'reportImport') return kind !== 'done' && project.canEdit;
|
|
if (page === 'binder' || page === 'worksImport') return project.canEdit;
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Текущий проект: id из адреса, а без него — сохранённый в сессии выбор.
|
|
* Данные (статус, права) берутся из свежего списка с сервера; сохранённая копия — только пока список
|
|
* не загрузился или если проекта в нём нет. Иначе после смены статуса / прав F5 показывал бы старые.
|
|
*/
|
|
export function resolveCurrentProject(
|
|
projects: IAccessibleProject[],
|
|
routeProjectId: number | undefined,
|
|
storedProject: IAccessibleProject | undefined
|
|
): IAccessibleProject | undefined {
|
|
const targetId = routeProjectId ?? storedProject?.id;
|
|
if (targetId === undefined) return undefined;
|
|
const fresh = projects.filter((project) => project.id === targetId)[0];
|
|
if (fresh) return fresh;
|
|
return storedProject?.id === targetId ? storedProject : undefined;
|
|
}
|
|
|
|
/** Выгрузка за период недоступна только для архивного проекта. */
|
|
export function isExportAvailable(project: IAccessibleProject): boolean {
|
|
return getStatusKind(project.status) !== 'archive';
|
|
}
|