feat: import reports from Excel

The /Import/StroyBot/Reports endpoint only validates the file, so the
check now runs as soon as a file is picked. When there are no errors,
"Import" builds the reports from the ids in the response and saves them
with the same Dataverse code as the report form: one report per date,
company, shift and object; resources nested under their work; idle and
repair hours as separate rows.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Тимур Городецкий
2026-10-22 17:01:27 +05:00
parent eb646193fa
commit 1ba67fe4c1
32 changed files with 1382 additions and 4 deletions
+3
View File
@@ -9,6 +9,7 @@ import { ImportWorksPage } from '../pages/import/ImportWorksPage';
import { ResourcesPage } from '../pages/resources/ResourcesPage';
import { BinderPage } from '../pages/binder/BinderPage';
import { WeatherPage } from '../pages/weather/WeatherPage';
import { ReportImportPage } from '../pages/reportImport/ReportImportPage';
/** Показывает страницу текущего раздела (раздел берётся из ?page=). */
export const PageOutlet: React.FC = () => {
@@ -31,6 +32,8 @@ export const PageOutlet: React.FC = () => {
return <BinderPage />;
case 'weather':
return <WeatherPage />;
case 'reportImport':
return <ReportImportPage />;
case 'hub':
default:
return <HubPage />;
@@ -11,6 +11,7 @@ describe('buildSidebarItems', () => {
'objects',
'reports',
'report',
'reportImport',
'import',
'resources',
'binder'
+1
View File
@@ -15,6 +15,7 @@ export const NAV_ITEMS: INavItem[] = [
{ page: 'objects', title: 'Объекты', icon: 'folder', needsProject: true },
{ page: 'reports', title: 'Отчёты по проекту', icon: 'clipboard', needsProject: true },
{ page: 'report', title: 'Отправить отчёт', icon: 'send', needsProject: true },
{ page: 'reportImport', title: 'Импорт отчёта из Excel', icon: 'upload', needsProject: true },
{ page: 'import', title: 'Загрузить работы из сметы', icon: 'download', needsProject: true },
{ page: 'resources', title: 'Ресурсы проекта', icon: 'people', needsProject: true },
{ page: 'binder', title: 'Привязка к справочнику', icon: 'swap', needsProject: true }
@@ -119,4 +119,10 @@ describe('isSectionAvailable', () => {
expect(isSectionAvailable('report', current)).toBe(true);
expect(isSectionAvailable('binder', current)).toBe(true);
});
it('импорт отчёта из Excel — по тем же правилам, что «Отправить отчёт»', () => {
expect(isSectionAvailable('reportImport', project({ status: 'текущий', canEdit: true }))).toBe(true);
expect(isSectionAvailable('reportImport', project({ status: 'текущий', canEdit: false }))).toBe(false);
expect(isSectionAvailable('reportImport', project({ status: 'завершён', canEdit: true }))).toBe(false);
});
});
@@ -73,6 +73,7 @@ export function getStatusKind(status: string | undefined): ProjectStatusKind {
* - архивный проект — закрыто всё (кроме архива, он не раздел приложения);
* - завершён — нельзя «Отправить отчёт»;
* - нет права edit0 — нельзя «Отправить отчёт».
* Импорт отчёта из Excel создаёт отчёты — те же правила, что у «Отправить отчёт».
* Привязка к справочнику — только с правом редактирования (решение при объединении).
*/
export function isSectionAvailable(page: PageId, project: IAccessibleProject): boolean {
@@ -80,7 +81,7 @@ export function isSectionAvailable(page: PageId, project: IAccessibleProject): b
const kind = getStatusKind(project.status);
if (kind === 'archive') return false;
if (page === 'report') return kind !== 'done' && project.canEdit;
if (page === 'report' || page === 'reportImport') return kind !== 'done' && project.canEdit;
if (page === 'binder') return project.canEdit;
return true;
}
+2 -1
View File
@@ -11,7 +11,8 @@ export const PAGE_IDS = [
'import',
'resources',
'binder',
'weather'
'weather',
'reportImport'
] as const;
export type PageId = (typeof PAGE_IDS)[number];