feat: port stroy-bot-main as the hub page
Project picker, section cards and the period Excel export now live on the app's home page. The selected project is shared app state, so the sidebar hides sections with the same rules main used to disable its cards (archived, finished, no edit0 right). Queries are 1:1 with stroy-bot-main and covered by tests against a fake HTTP client. Behaviour differences are listed in the migration checklist. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,91 @@
|
||||
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 — нельзя «Отправить отчёт».
|
||||
* Привязка к справочнику — только с правом редактирования (решение при объединении).
|
||||
*/
|
||||
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') return kind !== 'done' && project.canEdit;
|
||||
if (page === 'binder') return project.canEdit;
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Выгрузка за период недоступна только для архивного проекта. */
|
||||
export function isExportAvailable(project: IAccessibleProject): boolean {
|
||||
return getStatusKind(project.status) !== 'archive';
|
||||
}
|
||||
Reference in New Issue
Block a user