From bc1c54c43edf9142cda034dbc6cb81bdc8381b61 Mon Sep 17 00:00:00 2001 From: Alex Ant Date: Sat, 25 Oct 2025 20:19:10 +0500 Subject: [PATCH] notifications on novu improve (sanity), make a coop settings for chairman and select default workspace / page --- .cursor/rules/workflow-validation.mdc | 20 + .vscode/settings.json | 27 +- components/controller/schema.gql | 58 +- .../services/agenda-notification.service.ts | 54 +- .../services/decision-notification.service.ts | 16 +- .../desktop/dto/desktop-workspace.dto.ts | 8 + .../application/desktop/dto/desktop.dto.ts | 14 +- .../desktop/services/desktop.service.ts | 2 +- .../services/payment-notification.service.ts | 11 +- .../participant-notification.service.ts | 61 + .../services/participant.service.ts | 9 + .../application/system/dto/settings.dto.ts | 74 + .../src/application/system/dto/system.dto.ts | 6 + .../system/resolvers/system.resolver.ts | 12 + .../system/services/system.service.ts | 9 + .../services/wallet-notification.service.ts | 88 + .../wallet/services/wallet.service.ts | 13 +- .../src/application/wallet/wallet.module.ts | 6 +- .../desktop/entities/desktop-domain.entity.ts | 10 +- .../desktop/interactors/desktop.interactor.ts | 5 +- .../interfaces/desktop-domain.interface.ts | 6 +- .../controller/src/domain/domain.module.ts | 3 + .../interfaces/novu-workflow.port.ts | 8 - .../workflow-trigger-domain.interface.ts | 35 - .../entities/settings-domain.entity.ts | 63 + .../interactors/settings.interactor.ts | 48 + .../interfaces/settings-domain.interface.ts | 44 + .../update-settings-input-domain.interface.ts | 25 + .../repositories/settings.repository.ts | 33 + .../domain/settings/settings-domain.module.ts | 11 + .../src/domain/settings/settings.module.ts | 12 + .../entities/systeminfo-domain.entity.ts | 2 + .../system/interactors/system.interactor.ts | 25 +- .../src/domain/system/system-domain.module.ts | 3 +- .../dto/project_management/project.dto.ts | 2 +- .../application/dto/segments/segment.dto.ts | 6 + .../infrastructure/mappers/segment.mapper.ts | 22 +- .../approval-response-notification.service.ts | 2 + .../meet-workflow-notification.service.ts | 209 +- .../blockchain/blockchain-consumer.service.ts | 3 + .../typeorm/entities/settings.entity.ts | 69 + .../settings.typeorm-repository.ts | 56 + .../database/typeorm/typeorm.module.ts | 11 +- .../novu/novu-workflow.adapter.ts | 67 +- components/controller/tsconfig.json | 3 + components/cooptypes/tsconfig.json | 3 +- .../ui/Dialog/CreateProjectInvestDialog.vue | 91 + .../CreateProjectInvest/ui/Dialog/index.ts | 1 + .../ui/Fab/ComponentInvestFabAction.vue | 43 + .../ui/Fab/ProjectInvestFabAction.vue | 39 + .../CreateProjectInvest/ui/Fab/index.ts | 2 + .../Invest/CreateProjectInvest/ui/index.ts | 2 + .../ui/UpdatePriority/UpdatePriority.vue | 16 +- .../Project/AddAuthor/ui/AddAuthorButton.vue | 15 +- .../AddAuthor/ui/Dialog/AddAuthorDialog.vue | 130 ++ .../Project/AddAuthor/ui/Dialog/index.ts | 1 + .../AddAuthor/ui/Fab/AddAuthorFabAction.vue | 30 + .../Project/AddAuthor/ui/Fab/index.ts | 1 + .../features/Project/AddAuthor/ui/index.ts | 2 + .../SetPlan/ui/Dialog/SetPlanDialog.vue | 128 + .../Project/SetPlan/ui/Dialog/index.ts | 1 + .../SetPlan/ui/Fab/SetPlanFabAction.vue | 30 + .../features/Project/SetPlan/ui/Fab/index.ts | 1 + .../Project/SetPlan/ui/SetPlanButton.vue | 105 +- .../features/Project/SetPlan/ui/index.ts | 2 + .../ui/Dialog/CreateRequirementDialog.vue | 169 ++ .../Story/CreateStory/ui/Dialog/index.ts | 1 + .../ui/Fab/CreateRequirementFabAction.vue | 34 + .../Story/CreateStory/ui/Fab/index.ts | 1 + .../features/Story/CreateStory/ui/index.ts | 2 + .../DeleteStory/ui/DeleteStoryButton.vue | 6 - .../desktop/extensions/capital/install.ts | 28 - .../pages/ComponentAuthorsPage/index.ts | 1 - .../ui/ComponentAuthorsPage.vue | 70 - .../pages/ComponentAuthorsPage/ui/index.ts | 1 - .../ui/ComponentContributorsPage.vue | 3 - .../pages/ComponentPage/ui/ComponentPage.vue | 91 +- .../ui/ComponentPlanningPage.vue | 13 +- .../ui/ComponentRequirementsPage.vue | 21 +- .../ui/ComponentTasksPage.vue | 3 + .../capital/pages/IssuePage/ui/IssuePage.vue | 87 +- .../capital/pages/ProjectAuthorsPage/index.ts | 1 - .../ui/ProjectAuthorsPage.vue | 25 - .../ui/ProjectContributorsPage.vue | 3 - .../pages/ProjectPage/ui/ProjectPage.vue | 93 +- .../ui/ProjectPlanningPage.vue | 13 +- .../ui/ProjectRequirementsPage.vue | 21 +- .../ProjectsListPage/ui/ProjectsListPage.vue | 11 +- .../desktop/extensions/capital/pages/index.ts | 1 - .../shared/composables/useListNavigation.ts | 24 + .../ComponentsListWidget.vue | 30 +- .../IssueTitleEditor/IssueTitleEditor.vue | 76 + .../capital/widgets/IssueTitleEditor/index.ts | 1 + .../IssuesListWidget/ui/IssuesListWidget.vue | 30 +- .../ProjectAuthorsList.vue | 111 - .../ProjectContributorsList.vue | 134 +- .../ProjectInfoSelectorWidget.vue | 19 +- .../ProjectPlanning.vue | 243 -- .../ProjectPlanningWidget.vue | 98 + .../widgets/ProjectPlanningWidget/index.ts | 1 + .../ProjectsListWidget/ProjectsListWidget.vue | 42 +- .../widgets/RequirementsListWidget/index.ts | 1 + .../ui/RequirementsListWidget.vue | 272 +++ .../widgets/StoryWidget/ui/StoriesWidget.vue | 6 +- .../extensions/capital/widgets/index.ts | 2 + .../features/UpdateSettings/api/index.ts | 22 + .../chairman/features/UpdateSettings/index.ts | 3 + .../features/UpdateSettings/model/index.ts | 20 + .../UpdateSettings/ui/UpdateSettingsForm.vue | 240 ++ .../features/UpdateSettings/ui/index.ts | 1 + .../extensions/chairman/features/index.ts | 1 + .../desktop/extensions/chairman/install.ts | 13 + .../pages/SystemSettingsPage/index.ts | 1 + .../ui/SystemSettingsPage.vue | 86 + .../pages/SystemSettingsPage/ui/index.ts | 1 + .../extensions/chairman/pages/index.ts | 1 + .../src/entities/Desktop/model/store.ts | 72 +- .../ExtensionsManagement.vue | 2 +- .../PermissionDenied/PermissionDenied.vue | 10 +- .../processes/navigation-guard-setup/index.ts | 11 +- .../desktop/src/shared/ui/Editor/Editor.vue | 53 +- .../ExtensionDesktopsList/ui/DesktopsList.vue | 2 +- .../desktop/tsconfig.vue-tsc.tsbuildinfo | 1 + .../.cursor/rules/notifications.mdc | 20 + .../src/sync/novu-sync.service.ts | 118 +- components/notifications/src/utils/index.ts | 1 + .../src/utils/slugify/builtinReplacements.ts | 2065 +++++++++++++++++ .../src/utils/slugify/slugify.ts | 284 +++ .../src/utils/slugify/transliterate.ts | 48 + .../src/workflows/approval-request/index.ts | 8 +- .../src/workflows/approval-response/index.ts | 17 +- .../src/workflows/decision-approved/index.ts | 10 +- .../src/workflows/incoming-transfer/index.ts | 6 +- .../notifications/src/workflows/index.ts | 12 +- .../src/workflows/meet-ended/index.ts | 8 +- .../src/workflows/meet-initial/index.ts | 6 +- .../src/workflows/meet-reminder-end/index.ts | 6 +- .../workflows/meet-reminder-start/index.ts | 6 +- .../src/workflows/meet-restart/index.ts | 6 +- .../src/workflows/meet-started/index.ts | 6 +- .../src/workflows/new-agenda-item/index.ts | 12 +- .../new-deposit-payment-request/index.ts | 50 + .../new-initial-payment-request/index.ts | 50 + .../src/workflows/payment-cancelled/index.ts | 8 +- .../index.ts | 17 +- .../src/workflows/welcome/index.ts | 8 +- components/notifications/tsconfig.json | 2 +- components/sdk/src/mutations/system/index.ts | 5 +- .../src/mutations/system/updateSettings.ts | 19 + .../src/selectors/capital/segmentSelector.ts | 1 + .../src/selectors/desktop/desktopSelector.ts | 2 - .../selectors/system/systemInfoSelector.ts | 13 +- components/sdk/src/zeus/const.ts | 20 +- components/sdk/src/zeus/index.ts | 156 +- components/sdk/tsconfig.json | 3 +- components/sdk/tsconfig.tsbuildinfo | 2 +- tsconfig.json | 30 +- 157 files changed, 6154 insertions(+), 1114 deletions(-) create mode 100644 .cursor/rules/workflow-validation.mdc create mode 100644 components/controller/src/application/system/dto/settings.dto.ts create mode 100644 components/controller/src/application/wallet/services/wallet-notification.service.ts create mode 100644 components/controller/src/domain/settings/entities/settings-domain.entity.ts create mode 100644 components/controller/src/domain/settings/interactors/settings.interactor.ts create mode 100644 components/controller/src/domain/settings/interfaces/settings-domain.interface.ts create mode 100644 components/controller/src/domain/settings/interfaces/update-settings-input-domain.interface.ts create mode 100644 components/controller/src/domain/settings/repositories/settings.repository.ts create mode 100644 components/controller/src/domain/settings/settings-domain.module.ts create mode 100644 components/controller/src/domain/settings/settings.module.ts create mode 100644 components/controller/src/infrastructure/database/typeorm/entities/settings.entity.ts create mode 100644 components/controller/src/infrastructure/database/typeorm/repositories/settings.typeorm-repository.ts create mode 100644 components/desktop/extensions/capital/features/Invest/CreateProjectInvest/ui/Dialog/CreateProjectInvestDialog.vue create mode 100644 components/desktop/extensions/capital/features/Invest/CreateProjectInvest/ui/Dialog/index.ts create mode 100644 components/desktop/extensions/capital/features/Invest/CreateProjectInvest/ui/Fab/ComponentInvestFabAction.vue create mode 100644 components/desktop/extensions/capital/features/Invest/CreateProjectInvest/ui/Fab/ProjectInvestFabAction.vue create mode 100644 components/desktop/extensions/capital/features/Invest/CreateProjectInvest/ui/Fab/index.ts create mode 100644 components/desktop/extensions/capital/features/Project/AddAuthor/ui/Dialog/AddAuthorDialog.vue create mode 100644 components/desktop/extensions/capital/features/Project/AddAuthor/ui/Dialog/index.ts create mode 100644 components/desktop/extensions/capital/features/Project/AddAuthor/ui/Fab/AddAuthorFabAction.vue create mode 100644 components/desktop/extensions/capital/features/Project/AddAuthor/ui/Fab/index.ts create mode 100644 components/desktop/extensions/capital/features/Project/SetPlan/ui/Dialog/SetPlanDialog.vue create mode 100644 components/desktop/extensions/capital/features/Project/SetPlan/ui/Dialog/index.ts create mode 100644 components/desktop/extensions/capital/features/Project/SetPlan/ui/Fab/SetPlanFabAction.vue create mode 100644 components/desktop/extensions/capital/features/Project/SetPlan/ui/Fab/index.ts create mode 100644 components/desktop/extensions/capital/features/Story/CreateStory/ui/Dialog/CreateRequirementDialog.vue create mode 100644 components/desktop/extensions/capital/features/Story/CreateStory/ui/Dialog/index.ts create mode 100644 components/desktop/extensions/capital/features/Story/CreateStory/ui/Fab/CreateRequirementFabAction.vue create mode 100644 components/desktop/extensions/capital/features/Story/CreateStory/ui/Fab/index.ts delete mode 100644 components/desktop/extensions/capital/pages/ComponentAuthorsPage/index.ts delete mode 100644 components/desktop/extensions/capital/pages/ComponentAuthorsPage/ui/ComponentAuthorsPage.vue delete mode 100644 components/desktop/extensions/capital/pages/ComponentAuthorsPage/ui/index.ts delete mode 100644 components/desktop/extensions/capital/pages/ProjectAuthorsPage/index.ts delete mode 100644 components/desktop/extensions/capital/pages/ProjectAuthorsPage/ui/ProjectAuthorsPage.vue create mode 100644 components/desktop/extensions/capital/shared/composables/useListNavigation.ts create mode 100644 components/desktop/extensions/capital/widgets/IssueTitleEditor/IssueTitleEditor.vue create mode 100644 components/desktop/extensions/capital/widgets/IssueTitleEditor/index.ts delete mode 100644 components/desktop/extensions/capital/widgets/ProjectInfoSelectorWidget/ProjectAuthorsList.vue delete mode 100644 components/desktop/extensions/capital/widgets/ProjectInfoSelectorWidget/ProjectPlanning.vue create mode 100644 components/desktop/extensions/capital/widgets/ProjectPlanningWidget/ProjectPlanningWidget.vue create mode 100644 components/desktop/extensions/capital/widgets/ProjectPlanningWidget/index.ts create mode 100644 components/desktop/extensions/capital/widgets/RequirementsListWidget/index.ts create mode 100644 components/desktop/extensions/capital/widgets/RequirementsListWidget/ui/RequirementsListWidget.vue create mode 100644 components/desktop/extensions/chairman/features/UpdateSettings/api/index.ts create mode 100644 components/desktop/extensions/chairman/features/UpdateSettings/index.ts create mode 100644 components/desktop/extensions/chairman/features/UpdateSettings/model/index.ts create mode 100644 components/desktop/extensions/chairman/features/UpdateSettings/ui/UpdateSettingsForm.vue create mode 100644 components/desktop/extensions/chairman/features/UpdateSettings/ui/index.ts create mode 100644 components/desktop/extensions/chairman/pages/SystemSettingsPage/index.ts create mode 100644 components/desktop/extensions/chairman/pages/SystemSettingsPage/ui/SystemSettingsPage.vue create mode 100644 components/desktop/extensions/chairman/pages/SystemSettingsPage/ui/index.ts create mode 100644 components/desktop/tsconfig.vue-tsc.tsbuildinfo create mode 100644 components/notifications/.cursor/rules/notifications.mdc create mode 100644 components/notifications/src/utils/index.ts create mode 100644 components/notifications/src/utils/slugify/builtinReplacements.ts create mode 100644 components/notifications/src/utils/slugify/slugify.ts create mode 100644 components/notifications/src/utils/slugify/transliterate.ts create mode 100644 components/notifications/src/workflows/new-deposit-payment-request/index.ts create mode 100644 components/notifications/src/workflows/new-initial-payment-request/index.ts rename components/notifications/src/workflows/{payment-completed => payment-paid}/index.ts (65%) create mode 100644 components/sdk/src/mutations/system/updateSettings.ts diff --git a/.cursor/rules/workflow-validation.mdc b/.cursor/rules/workflow-validation.mdc new file mode 100644 index 00000000000..008d6bee0b5 --- /dev/null +++ b/.cursor/rules/workflow-validation.mdc @@ -0,0 +1,20 @@ +--- +globs: notifications/src/workflows/**/*.ts +--- +# Правила валидации воркфлоу + +## 🚫 ID воркфлоу +- **Длина ID не должна превышать 32 символа** +- Используйте описательные, но короткие идентификаторы малыми латинскими буквами и тире. + +## 🚫 Условия в шаблонах +- **Запрещено использовать JavaScript выражения в шаблонах Novu** +- Нельзя использовать: + - Тернарные операторы: `{{condition ? "text1" : "text2"}}` + - Логические операторы: `{{field && "text"}}` + - Любые другие JS конструкции + +## ✅ Рекомендации +- Добавляйте текстовые поля в payload для условной логики +- Вычисляйте значения на стороне сервера перед отправкой уведомления +- Используйте только простые переменные: `{{payload.fieldName}}` \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index 5a4443a76a3..a633fe1a66e 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -13,9 +13,7 @@ // Оптимизация для монорепозитория "typescript.preferences.useAliasesForRenames": false, - "typescript.preferences.include": false, - "typescript.disableAutomaticTypeAcquisition": true, - "typescript.preferences.includePackageJsonAutoImports": "off", + "typescript.preferences.includePackageJsonAutoImports": "on", "typescript.suggest.autoImports": true, "typescript.suggest.paths": true, "typescript.updateImportsOnFileMove.enabled": "always", @@ -47,13 +45,15 @@ "**/node_modules/.cache": true }, - // TypeScript server настройки - "typescript.tsserver.maxTsServerMemory": 4000, + // TypeScript server настройки для монорепозитория + "typescript.tsserver.maxTsServerMemory": 8192, "typescript.tsserver.watchOptions": { - "excludeFiles": [ - "**/node_modules/**/*", - "**/dist/**/*", - "**/.cache/**/*" + "excludeDirectories": [ + "**/node_modules", + "**/dist", + "**/.cache", + "**/.quasar", + "**/build" ] }, @@ -61,6 +61,11 @@ "typescript.tsdk": "node_modules/typescript/lib", "typescript.enablePromptUseWorkspaceTsdk": true, - // Отключить автоматическую инициализацию для всех tsconfig - "typescript.tsserver.experimental.enableProjectDiagnostics": false + // КРИТИЧЕСКИ ВАЖНО: включить project references для монорепозитория + "typescript.tsserver.useSyntaxServer": "auto", + "typescript.tsserver.experimental.enableProjectDiagnostics": true, + + // Оптимизация для больших монорепозиториев + "typescript.disableAutomaticTypeAcquisition": true, + "typescript.surveys.enabled": false } diff --git a/components/controller/schema.gql b/components/controller/schema.gql index a37659cacc9..465da75ae43 100644 --- a/components/controller/schema.gql +++ b/components/controller/schema.gql @@ -31,7 +31,7 @@ type Account { private_account: PrivateAccount """ - объект аккаунта в системе учёта провайдера, т.е. MONO. Здесь хранится приватная информация о пайщике кооператива, которая содержит его приватные данные. Эти данные не публикуются в блокчейне и не выходят за пределы базы данных провайдера. Они используются для заполнения шаблонов документов при нажатии соответствующих кнопок на платформе. + объект аккаунта в системе учёта провайдера, т.е. MONO. Здесь хранится приватная информация о пайщике кооператива, которая содержит его приватные данные. Эти данные не публикуются в блокчейне и не выходят за пределы базы данных провайдера. Они используются для заполнения шаблонов документов при нажатии соответствующих кнопок на платформе. """ provider_account: MonoAccount @@ -2473,7 +2473,7 @@ type CapitalProjectFactPool { """Общая сумма""" total: String! - """Общий объем вклада""" + """Общий объем взноса старших участников""" total_contribution: String! """Общий генерационный пул""" @@ -2914,6 +2914,9 @@ type CapitalSegment { """Имя пользователя""" username: String! + """Вклад участника словами участника""" + value: String + """Бонус голосования""" voting_bonus: String! } @@ -4318,18 +4321,12 @@ input DeltaFiltersInput { } type Desktop { - """Имя шаблона рабочих столов""" - authorizedHome: String! - """Имя аккаунта кооператива""" coopname: String! """Имя шаблона рабочих столов""" layout: String! - """Имя шаблона рабочих столов""" - nonAuthorizedHome: String! - """Состав приложений рабочего стола""" workspaces: [DesktopWorkspace!]! } @@ -6111,6 +6108,9 @@ type Mutation { """Обновить заявку""" updateRequest(data: UpdateRequestInput!): Transaction! + """Обновить настройки системы (рабочие столы и маршруты по умолчанию)""" + updateSettings(data: UpdateSettingsInput!): Settings! + """Обновить параметры системы""" updateSystem(data: Update!): SystemInfo! @@ -8171,6 +8171,29 @@ input SetWifInput { wif: String! } +type Settings { + """Маршрут по умолчанию для авторизованных пользователей""" + authorized_default_route: String! + + """Рабочий стол по умолчанию для авторизованных пользователей""" + authorized_default_workspace: String! + + """Название кооператива""" + coopname: String! + + """Дата создания""" + created_at: DateTime! + + """Маршрут по умолчанию для неавторизованных пользователей""" + non_authorized_default_route: String! + + """Рабочий стол по умолчанию для неавторизованных пользователей""" + non_authorized_default_workspace: String! + + """Дата последнего обновления""" + updated_at: DateTime! +} + input SignActAsChairmanInput { """Акт о вкладе результатов""" act: SignedDigitalDocumentInput! @@ -8427,6 +8450,9 @@ type SystemInfo { """Имя аккаунта кооператива""" coopname: String! + """Настройки системы""" + settings: Settings! + """Символы и их точности блокчейна""" symbols: Symbols! @@ -8714,6 +8740,20 @@ input UpdateRequestInput { username: String! } +input UpdateSettingsInput { + """Маршрут по умолчанию для авторизованных пользователей""" + authorized_default_route: String + + """Рабочий стол по умолчанию для авторизованных пользователей""" + authorized_default_workspace: String + + """Маршрут по умолчанию для неавторизованных пользователей""" + non_authorized_default_route: String + + """Рабочий стол по умолчанию для неавторизованных пользователей""" + non_authorized_default_workspace: String +} + input UpdateStoryInput { """Описание истории""" description: String @@ -8941,4 +8981,4 @@ input WebPushSubscriptionKeysInput { """P256DH ключ для шифрования""" p256dh: String! -} +} \ No newline at end of file diff --git a/components/controller/src/application/agenda/services/agenda-notification.service.ts b/components/controller/src/application/agenda/services/agenda-notification.service.ts index ddc423742d2..0197c830812 100644 --- a/components/controller/src/application/agenda/services/agenda-notification.service.ts +++ b/components/controller/src/application/agenda/services/agenda-notification.service.ts @@ -7,10 +7,8 @@ import { ACCOUNT_EXTENSION_PORT, AccountExtensionPort } from '~/domain/extension import config from '~/config/config'; import { SovietContract } from 'cooptypes'; import type { ActionDomainInterface } from '~/domain/parser/interfaces/action-domain.interface'; -import type { - WorkflowBulkTriggerDomainInterface, - WorkflowBulkEventDomainInterface, -} from '~/domain/notification/interfaces/workflow-trigger-domain.interface'; +import type { WorkflowTriggerDomainInterface } from '~/domain/notification/interfaces/workflow-trigger-domain.interface'; +import { Workflows } from '@coopenomics/notifications'; /** * Сервис для отправки уведомлений о новых вопросах на повестке совета @@ -41,14 +39,15 @@ export class AgendaNotificationService implements OnModuleInit { @OnEvent(`action::${SovietContract.contractName.production}::${SovietContract.Actions.Decisions.CreateAgenda.actionName}`) async handleCreateAgenda(actionData: ActionDomainInterface): Promise { try { - const action = actionData.data as any; + const action = actionData.data as SovietContract.Interfaces.ICreateagenda; + const agendaId = action.hash.substring(0, 4); // Проверяем что это наш кооператив if (action.coopname !== config.coopname) { return; } - this.logger.debug(`Обработка создания нового вопроса на повестке для решения ${action.decision_id}`); + this.logger.debug(`Обработка создания нового вопроса на повестке для решения ${agendaId}`); // Получаем членов совета (chairman и member) через порт // Получаем председателей @@ -71,19 +70,18 @@ export class AgendaNotificationService implements OnModuleInit { const authorName = await this.accountPort.getDisplayName(action.username); // Формируем данные для workflow - const payload = { + const payload: Workflows.NewAgenda.IPayload = { coopname: action.coopname, coopShortName, - itemTitle: `Вопрос №${action.decision_id}`, - itemDescription: `Новый вопрос добавлен на повестку заседания совета`, + itemTitle: `№${agendaId}`, //TODO: по типу запроса + itemDescription: ``, //TODO: по типу запроса authorName, - decision_id: action.decision_id, - agendaUrl: `${config.base_url}/${action.coopname}/council/agenda`, + decision_id: agendaId, + agendaUrl: `${config.base_url}/${action.coopname}/soviet/agenda`, }; - // Подготавливаем события для пакетной отправки - const events: WorkflowBulkEventDomainInterface[] = []; - + // Отправляем уведомления каждому члену совета в цикле + let sentCount = 0; for (const member of allCouncilMembers) { const memberEmail = member.provider_account?.email; if (!memberEmail) { @@ -91,30 +89,28 @@ export class AgendaNotificationService implements OnModuleInit { continue; } - events.push({ + const triggerData: WorkflowTriggerDomainInterface = { + name: Workflows.NewAgenda.id, to: { subscriberId: member.username, email: memberEmail, }, payload, - }); + }; + + try { + await this.novuWorkflowAdapter.triggerWorkflow(triggerData); + sentCount++; + } catch (error: any) { + this.logger.error(`Ошибка отправки уведомления члену совета ${member.username}: ${error.message}`); + } } - if (events.length === 0) { - this.logger.warn('Нет подходящих получателей для уведомления'); + if (sentCount === 0) { + this.logger.warn('Не удалось отправить уведомления ни одному члену совета'); return; } - - // Отправляем уведомления пакетом - const bulkTriggerData: WorkflowBulkTriggerDomainInterface = { - name: 'noviy-vopros-na-povestke', - events, - }; - - await this.novuWorkflowAdapter.triggerBulkWorkflow(bulkTriggerData); - this.logger.log( - `Уведомления отправлены ${events.length} членам совета о новом вопросе на повестке (решение ${action.decision_id})` - ); + this.logger.log(`Уведомления отправлены ${sentCount} членам совета о новом вопросе на повестке (решение ${agendaId})`); } catch (error: any) { this.logger.error(`Ошибка при обработке создания вопроса на повестке: ${error.message}`, error.stack); } diff --git a/components/controller/src/application/agenda/services/decision-notification.service.ts b/components/controller/src/application/agenda/services/decision-notification.service.ts index 899d8275972..2ee0e866b02 100644 --- a/components/controller/src/application/agenda/services/decision-notification.service.ts +++ b/components/controller/src/application/agenda/services/decision-notification.service.ts @@ -39,19 +39,19 @@ export class DecisionNotificationService implements OnModuleInit { @OnEvent(`action::${SovietContract.contractName.production}::newdecision`) async handleNewDecision(actionData: ActionDomainInterface): Promise { try { - const action = actionData.data as any; - + const action = actionData.data as SovietContract.Interfaces.INewdecision; + const decisionId = action.package.substring(0, 4); // Проверяем что это наш кооператив if (action.coopname !== config.coopname) { return; } - this.logger.debug(`Обработка нового решения совета: ${action.decision_id}`); + this.logger.debug(`Обработка нового решения совета: ${action.package}`); // Получаем username из действия const username = action.username; if (!username) { - this.logger.warn(`Username не найден в действии ${action.decision_id}`); + this.logger.warn(`Username не найден в действии ${action.package}`); return; } @@ -69,10 +69,10 @@ export class DecisionNotificationService implements OnModuleInit { // Формируем данные для workflow (без приватных данных) const payload: Workflows.DecisionApproved.IPayload = { userName, - decisionTitle: `Решение №${action.decision_id}`, + decisionTitle: `Решение №${decisionId}`, coopname: action.coopname, - decision_id: action.decision_id, - decisionUrl: `${config.base_url}/${action.coopname}/user/decisions/${action.decision_id}`, + decision_id: decisionId, + decisionUrl: `${config.base_url}`, }; // Отправляем уведомление @@ -86,7 +86,7 @@ export class DecisionNotificationService implements OnModuleInit { }; await this.novuWorkflowAdapter.triggerWorkflow(triggerData); - this.logger.log(`Уведомление отправлено пользователю ${username} о принятии решения ${action.decision_id}`); + this.logger.log(`Уведомление отправлено пользователю ${username} о принятии решения ${decisionId}`); } catch (error: any) { this.logger.error(`Ошибка при обработке нового решения совета: ${error.message}`, error.stack); } diff --git a/components/controller/src/application/desktop/dto/desktop-workspace.dto.ts b/components/controller/src/application/desktop/dto/desktop-workspace.dto.ts index 382c767b30e..c7e27dbec83 100644 --- a/components/controller/src/application/desktop/dto/desktop-workspace.dto.ts +++ b/components/controller/src/application/desktop/dto/desktop-workspace.dto.ts @@ -25,4 +25,12 @@ export class DesktopWorkspaceDTO implements DesktopWorkspaceDomainInterface { @IsOptional() @IsString() public readonly defaultRoute?: string; + + constructor(data: DesktopWorkspaceDomainInterface) { + this.name = data.name; + this.title = data.title; + this.extension_name = data.extension_name; + this.icon = data.icon; + this.defaultRoute = data.defaultRoute; + } } diff --git a/components/controller/src/application/desktop/dto/desktop.dto.ts b/components/controller/src/application/desktop/dto/desktop.dto.ts index 2bffda1c07d..fbaed473ccc 100644 --- a/components/controller/src/application/desktop/dto/desktop.dto.ts +++ b/components/controller/src/application/desktop/dto/desktop.dto.ts @@ -14,16 +14,14 @@ export class DesktopDTO implements DesktopDomainInterface { @IsString() public readonly layout!: string; - @Field(() => String, { description: 'Имя шаблона рабочих столов' }) - @IsString() - public readonly authorizedHome!: string; - - @Field(() => String, { description: 'Имя шаблона рабочих столов' }) - @IsString() - public readonly nonAuthorizedHome!: string; - @Field(() => [DesktopWorkspaceDTO], { description: 'Состав приложений рабочего стола' }) @ValidateNested({ each: true }) @Type(() => DesktopWorkspaceDTO) public readonly workspaces!: DesktopWorkspaceDTO[]; + + constructor(data: DesktopDomainInterface) { + this.coopname = data.coopname; + this.layout = data.layout; + this.workspaces = data.workspaces.map((workspace) => new DesktopWorkspaceDTO(workspace)); + } } diff --git a/components/controller/src/application/desktop/services/desktop.service.ts b/components/controller/src/application/desktop/services/desktop.service.ts index a81f3f280fa..0f723e058c8 100644 --- a/components/controller/src/application/desktop/services/desktop.service.ts +++ b/components/controller/src/application/desktop/services/desktop.service.ts @@ -9,6 +9,6 @@ export class DesktopService { public async getDesktop(): Promise { const desktop = await this.desktopDomainInteractor.getDesktop(); - return desktop; + return new DesktopDTO(desktop); } } diff --git a/components/controller/src/application/gateway/services/payment-notification.service.ts b/components/controller/src/application/gateway/services/payment-notification.service.ts index 3453bdd968e..7f5380a5f07 100644 --- a/components/controller/src/application/gateway/services/payment-notification.service.ts +++ b/components/controller/src/application/gateway/services/payment-notification.service.ts @@ -33,8 +33,8 @@ export class PaymentNotificationService implements OnModuleInit { */ async notifyPaymentStatus(payment: PaymentDomainEntity): Promise { try { - // Отправляем уведомления только для завершенных и отмененных платежей - if (payment.status !== PaymentStatusEnum.COMPLETED && payment.status !== PaymentStatusEnum.CANCELLED) { + // Отправляем уведомления только для оплаченных и отмененных платежей + if (payment.status !== PaymentStatusEnum.PAID && payment.status !== PaymentStatusEnum.CANCELLED) { return; } @@ -53,16 +53,15 @@ export class PaymentNotificationService implements OnModuleInit { // Выбираем workflow в зависимости от статуса платежа const workflowId = - payment.status === PaymentStatusEnum.COMPLETED ? Workflows.PaymentCompleted.id : Workflows.PaymentCancelled.id; + payment.status === PaymentStatusEnum.PAID ? Workflows.PaymentPaid.id : Workflows.PaymentCancelled.id; // Формируем данные для workflow (без приватных данных) - const payload: Workflows.PaymentCompleted.IPayload | Workflows.PaymentCancelled.IPayload = { + const payload: Workflows.PaymentPaid.IPayload | Workflows.PaymentCancelled.IPayload = { userName, paymentAmount: payment.quantity.toFixed(2), paymentCurrency: payment.symbol, - paymentId: payment.id || '', paymentDate: payment.created_at.toLocaleString('ru-RU'), - paymentUrl: `${config.base_url}/${payment.coopname}/user/payments/${payment.id}`, + paymentUrl: `${config.base_url}`, //TODO: точную ссылку потом }; // Отправляем уведомление diff --git a/components/controller/src/application/participant/services/participant-notification.service.ts b/components/controller/src/application/participant/services/participant-notification.service.ts index 1405415c500..c2d5c56f48c 100644 --- a/components/controller/src/application/participant/services/participant-notification.service.ts +++ b/components/controller/src/application/participant/services/participant-notification.service.ts @@ -5,6 +5,7 @@ import { NOVU_WORKFLOW_PORT } from '~/domain/notification/interfaces/novu-workfl import { ACCOUNT_EXTENSION_PORT, AccountExtensionPort } from '~/domain/extension/ports/account-extension-port'; import type { WorkflowTriggerDomainInterface } from '~/domain/notification/interfaces/workflow-trigger-domain.interface'; import { Workflows } from '@coopenomics/notifications'; +import config from '~/config/config'; /** * Сервис для отправки уведомлений участникам @@ -64,4 +65,64 @@ export class ParticipantNotificationService implements OnModuleInit { this.logger.error(`Ошибка при отправке приветственного уведомления: ${error.message}`, error.stack); } } + + /** + * Отправить уведомление председателю о новом заявке на вступительный взнос + */ + async sendNewInitialPaymentNotification( + participantUsername: string, + paymentAmount: string, + paymentCurrency: string, + coopname: string + ): Promise { + try { + this.logger.debug(`Отправка уведомления председателю о новой заявке на вступительный взнос от ${participantUsername}`); + + // Получаем председателя через порт + const chairmen = await this.accountPort.getAccounts({ role: 'chairman' }, { page: 1, limit: 1, sortOrder: 'ASC' }); + if (!chairmen.items || chairmen.items.length === 0) { + this.logger.warn('Председатель не найден, пропускаем отправку уведомления'); + return; + } + + const chairman = chairmen.items[0]; + const chairmanEmail = chairman.provider_account?.email; + if (!chairmanEmail) { + this.logger.warn(`Email председателя ${chairman.username} не найден`); + return; + } + + // Получаем отображаемые имена + const chairmanName = await this.accountPort.getDisplayName(chairman.username); + const participantName = await this.accountPort.getDisplayName(participantUsername); + + // Формируем данные для workflow + const payload: Workflows.NewInitialPaymentRequest.IPayload = { + chairmanName, + participantName, + paymentAmount, + paymentCurrency, + paymentType: 'Вступительный и минимальный паевой взнос', + coopname, + paymentUrl: `${config.base_url}`, + }; + + // Отправляем уведомление + const triggerData: WorkflowTriggerDomainInterface = { + name: Workflows.NewInitialPaymentRequest.id, + to: { + subscriberId: chairman.username, + email: chairmanEmail, + }, + payload, + }; + + await this.novuWorkflowAdapter.triggerWorkflow(triggerData); + this.logger.log( + `Уведомление отправлено председателю ${chairman.username} о новой заявке на вступительный взнос от ${participantUsername}` + ); + } catch (error: any) { + this.logger.error(`Ошибка при отправке уведомления о новом вступительном взносе: ${error.message}`, error.stack); + } + } } diff --git a/components/controller/src/application/participant/services/participant.service.ts b/components/controller/src/application/participant/services/participant.service.ts index 697f074a2ce..18e7dc6f77d 100644 --- a/components/controller/src/application/participant/services/participant.service.ts +++ b/components/controller/src/application/participant/services/participant.service.ts @@ -62,6 +62,15 @@ export class ParticipantService { public async createInitialPayment(data: CreateInitialPaymentInputDTO): Promise { const result = await this.participantDomainInteractor.createInitialPayment(data); const usernameCertificate = await this.userCertificateInteractor.getCertificateByUsername(result.username); + + // Отправляем уведомление председателю о новой заявке на вступительный взнос + await this.participantNotificationService.sendNewInitialPaymentNotification( + result.username, + result.quantity.toFixed(2), + result.symbol, + result.coopname + ); + return result.toDTO(usernameCertificate); } } diff --git a/components/controller/src/application/system/dto/settings.dto.ts b/components/controller/src/application/system/dto/settings.dto.ts new file mode 100644 index 00000000000..ac727fd0225 --- /dev/null +++ b/components/controller/src/application/system/dto/settings.dto.ts @@ -0,0 +1,74 @@ +import { ObjectType, Field, InputType } from '@nestjs/graphql'; +import { IsOptional, IsString } from 'class-validator'; +import type { SettingsDomainInterface } from '~/domain/settings/interfaces/settings-domain.interface'; +import type { UpdateSettingsInputDomainInterface } from '~/domain/settings/interfaces/update-settings-input-domain.interface'; + +/** + * DTO для настроек системы + * Используется для отображения настроек в GraphQL API + */ +@ObjectType('Settings') +export class SettingsDTO implements SettingsDomainInterface { + @Field(() => String, { description: 'Название кооператива' }) + @IsString() + coopname!: string; + + @Field(() => String, { description: 'Рабочий стол по умолчанию для авторизованных пользователей' }) + @IsString() + authorized_default_workspace!: string; + + @Field(() => String, { description: 'Маршрут по умолчанию для авторизованных пользователей' }) + @IsString() + authorized_default_route!: string; + + @Field(() => String, { description: 'Рабочий стол по умолчанию для неавторизованных пользователей' }) + @IsString() + non_authorized_default_workspace!: string; + + @Field(() => String, { description: 'Маршрут по умолчанию для неавторизованных пользователей' }) + @IsString() + non_authorized_default_route!: string; + + @Field(() => Date, { description: 'Дата создания' }) + created_at!: Date; + + @Field(() => Date, { description: 'Дата последнего обновления' }) + updated_at!: Date; + + constructor(data: SettingsDomainInterface) { + this.coopname = data.coopname; + this.authorized_default_workspace = data.authorized_default_workspace; + this.authorized_default_route = data.authorized_default_route; + this.non_authorized_default_workspace = data.non_authorized_default_workspace; + this.non_authorized_default_route = data.non_authorized_default_route; + this.created_at = data.created_at; + this.updated_at = data.updated_at; + } +} + +/** + * DTO для обновления настроек системы + * Все поля опциональны для частичного обновления + */ +@InputType('UpdateSettingsInput') +export class UpdateSettingsInputDTO implements UpdateSettingsInputDomainInterface { + @Field(() => String, { nullable: true, description: 'Рабочий стол по умолчанию для авторизованных пользователей' }) + @IsString() + @IsOptional() + authorized_default_workspace?: string; + + @Field(() => String, { nullable: true, description: 'Маршрут по умолчанию для авторизованных пользователей' }) + @IsString() + @IsOptional() + authorized_default_route?: string; + + @Field(() => String, { nullable: true, description: 'Рабочий стол по умолчанию для неавторизованных пользователей' }) + @IsString() + @IsOptional() + non_authorized_default_workspace?: string; + + @Field(() => String, { nullable: true, description: 'Маршрут по умолчанию для неавторизованных пользователей' }) + @IsString() + @IsOptional() + non_authorized_default_route?: string; +} diff --git a/components/controller/src/application/system/dto/system.dto.ts b/components/controller/src/application/system/dto/system.dto.ts index 58c3e9c97d1..48a1449f6e6 100644 --- a/components/controller/src/application/system/dto/system.dto.ts +++ b/components/controller/src/application/system/dto/system.dto.ts @@ -9,6 +9,7 @@ import { Type } from 'class-transformer'; import { ContactsDTO } from './contacts.dto'; import { VarsDTO } from './vars.dto'; import { SymbolsDTO } from './symbols.dto'; +import { SettingsDTO } from './settings.dto'; @ObjectType('SystemInfo') export class SystemInfoDTO { @@ -46,6 +47,10 @@ export class SystemInfoDTO { @ValidateNested() public readonly symbols: SymbolsDTO; + @Field(() => SettingsDTO, { description: 'Настройки системы' }) + @ValidateNested() + public readonly settings: SettingsDTO; + constructor(entity: SystemInfoDomainEntity) { this.coopname = entity.coopname; this.contacts = entity.contacts; @@ -53,6 +58,7 @@ export class SystemInfoDTO { this.blockchain_info = new BlockchainInfoDTO(entity.blockchain_info); this.system_status = entity.system_status as SystemStatus; this.symbols = entity.symbols; + this.settings = new SettingsDTO(entity.settings); this.blockchain_account = entity.blockchain_account; this.cooperator_account = new CooperativeOperatorAccountDTO({ ...entity.cooperator_account, diff --git a/components/controller/src/application/system/resolvers/system.resolver.ts b/components/controller/src/application/system/resolvers/system.resolver.ts index b7ac7ad9ed3..db772ba6099 100644 --- a/components/controller/src/application/system/resolvers/system.resolver.ts +++ b/components/controller/src/application/system/resolvers/system.resolver.ts @@ -5,6 +5,7 @@ import { InstallDTO } from '../dto/install.dto'; import { InitDTO } from '../dto/init.dto'; import { SetWifInputDTO } from '../dto/set-wif-input.dto'; import { UpdateDTO } from '../dto/update.dto'; +import { SettingsDTO, UpdateSettingsInputDTO } from '../dto/settings.dto'; @Resolver(() => SystemInfoDTO) export class SystemResolver { @@ -62,4 +63,15 @@ export class SystemResolver { await this.systemService.setWif(data); return true; } + + @Mutation(() => SettingsDTO, { + name: 'updateSettings', + description: 'Обновить настройки системы (рабочие столы и маршруты по умолчанию)', + }) + async updateSettings( + @Args('data', { type: () => UpdateSettingsInputDTO }) + data: UpdateSettingsInputDTO + ): Promise { + return this.systemService.updateSettings(data); + } } diff --git a/components/controller/src/application/system/services/system.service.ts b/components/controller/src/application/system/services/system.service.ts index 0f7ebdc11c5..055d447aa19 100644 --- a/components/controller/src/application/system/services/system.service.ts +++ b/components/controller/src/application/system/services/system.service.ts @@ -7,6 +7,7 @@ import type { SetWifInputDTO } from '../dto/set-wif-input.dto'; import type { UpdateDTO } from '../dto/update.dto'; import { OrganizationDomainEntity } from '~/domain/branch/entities/organization-domain.entity'; import config from '~/config/config'; +import { SettingsDTO, UpdateSettingsInputDTO } from '../dto/settings.dto'; @Injectable() export class SystemService { @@ -41,4 +42,12 @@ export class SystemService { public async setWif(data: SetWifInputDTO): Promise { await this.systemDomainInteractor.setWif(data); } + + /** + * Обновляет настройки системы + */ + public async updateSettings(data: UpdateSettingsInputDTO): Promise { + const settings = await this.systemDomainInteractor.updateSettings(data); + return new SettingsDTO(settings); + } } diff --git a/components/controller/src/application/wallet/services/wallet-notification.service.ts b/components/controller/src/application/wallet/services/wallet-notification.service.ts new file mode 100644 index 00000000000..1ac2d58cbe2 --- /dev/null +++ b/components/controller/src/application/wallet/services/wallet-notification.service.ts @@ -0,0 +1,88 @@ +import { Injectable, Inject, OnModuleInit } from '@nestjs/common'; +import { WinstonLoggerService } from '~/application/logger/logger-app.service'; +import { NovuWorkflowAdapter } from '~/infrastructure/novu/novu-workflow.adapter'; +import { NOVU_WORKFLOW_PORT } from '~/domain/notification/interfaces/novu-workflow.port'; +import { ACCOUNT_EXTENSION_PORT, AccountExtensionPort } from '~/domain/extension/ports/account-extension-port'; +import type { WorkflowTriggerDomainInterface } from '~/domain/notification/interfaces/workflow-trigger-domain.interface'; +import { Workflows } from '@coopenomics/notifications'; +import config from '~/config/config'; + +/** + * Сервис для отправки уведомлений по wallet модулю + */ +@Injectable() +export class WalletNotificationService implements OnModuleInit { + constructor( + @Inject(NOVU_WORKFLOW_PORT) + private readonly novuWorkflowAdapter: NovuWorkflowAdapter, + @Inject(ACCOUNT_EXTENSION_PORT) + private readonly accountPort: AccountExtensionPort, + private readonly logger: WinstonLoggerService + ) { + this.logger.setContext(WalletNotificationService.name); + } + + async onModuleInit() { + this.logger.log('WalletNotificationService инициализирован'); + } + + /** + * Отправить уведомление председателю о новом заявке на паевой взнос + */ + async sendNewDepositPaymentNotification( + participantUsername: string, + paymentAmount: string, + paymentCurrency: string, + coopname: string + ): Promise { + try { + this.logger.debug(`Отправка уведомления председателю о новой заявке на паевой взнос от ${participantUsername}`); + + // Получаем председателя через порт + const chairmen = await this.accountPort.getAccounts({ role: 'chairman' }, { page: 1, limit: 1, sortOrder: 'ASC' }); + if (!chairmen.items || chairmen.items.length === 0) { + this.logger.warn('Председатель не найден, пропускаем отправку уведомления'); + return; + } + + const chairman = chairmen.items[0]; + const chairmanEmail = chairman.provider_account?.email; + if (!chairmanEmail) { + this.logger.warn(`Email председателя ${chairman.username} не найден`); + return; + } + + // Получаем отображаемые имена + const chairmanName = await this.accountPort.getDisplayName(chairman.username); + const participantName = await this.accountPort.getDisplayName(participantUsername); + + // Формируем данные для workflow + const payload: Workflows.NewDepositPaymentRequest.IPayload = { + chairmanName, + participantName, + paymentAmount, + paymentCurrency, + paymentType: 'Паевой взнос по соглашению о ЦПП "Цифровой Кошелёк"', + coopname, + paymentUrl: `${config.base_url}`, + }; + + // Отправляем уведомление + const triggerData: WorkflowTriggerDomainInterface = { + name: Workflows.NewDepositPaymentRequest.id, + to: { + subscriberId: chairman.username, + email: chairmanEmail, + }, + payload, + }; + + await this.novuWorkflowAdapter.triggerWorkflow(triggerData); + this.logger.log( + `Уведомление отправлено председателю ${chairman.username} о новой заявке на паевой взнос от ${participantUsername}` + ); + } catch (error: any) { + this.logger.error(`Ошибка при отправке уведомления о новом паевом взносе: ${error.message}`, error.stack); + } + } +} diff --git a/components/controller/src/application/wallet/services/wallet.service.ts b/components/controller/src/application/wallet/services/wallet.service.ts index 8e7dc972f34..2ca30f4c00e 100644 --- a/components/controller/src/application/wallet/services/wallet.service.ts +++ b/components/controller/src/application/wallet/services/wallet.service.ts @@ -1,6 +1,7 @@ import { Injectable } from '@nestjs/common'; import { WalletDomainInteractor } from '~/domain/wallet/interactors/wallet.interactor'; import { UserCertificateInteractor } from '~/domain/user-certificate/interactors/user-certificate.interactor'; +import { WalletNotificationService } from './wallet-notification.service'; import { GenerateDocumentOptionsInputDTO } from '~/application/document/dto/generate-document-options-input.dto'; import { GeneratedDocumentDTO } from '~/application/document/dto/generated-document.dto'; import { ReturnByMoneyGenerateDocumentInputDTO } from '~/application/document/documents-dto/return-by-money-statement.dto'; @@ -18,7 +19,8 @@ import type { GatewayPaymentDTO } from '../../gateway/dto/gateway-payment.dto'; export class WalletService { constructor( private readonly walletDomainInteractor: WalletDomainInteractor, - private readonly userCertificateInteractor: UserCertificateInteractor + private readonly userCertificateInteractor: UserCertificateInteractor, + private readonly walletNotificationService: WalletNotificationService ) {} /** @@ -27,6 +29,15 @@ export class WalletService { public async createDepositPayment(data: CreateDepositPaymentInputDTO): Promise { const result = await this.walletDomainInteractor.createDepositPayment(data); const usernameCertificate = await this.userCertificateInteractor.getCertificateByUsername(result.username); + + // Отправляем уведомление председателю о новой заявке на паевой взнос + await this.walletNotificationService.sendNewDepositPaymentNotification( + result.username, + result.quantity.toFixed(2), + result.symbol, + result.coopname + ); + return result.toDTO(usernameCertificate); } diff --git a/components/controller/src/application/wallet/wallet.module.ts b/components/controller/src/application/wallet/wallet.module.ts index 17810888f2e..a5a7377bcdc 100644 --- a/components/controller/src/application/wallet/wallet.module.ts +++ b/components/controller/src/application/wallet/wallet.module.ts @@ -1,15 +1,17 @@ import { Module } from '@nestjs/common'; import { WalletResolver } from './resolvers/wallet.resolver'; import { WalletService } from './services/wallet.service'; +import { WalletNotificationService } from './services/wallet-notification.service'; import { WalletDomainInteractor } from '~/domain/wallet/interactors/wallet.interactor'; import { DomainModule } from '~/domain/domain.module'; +import { NovuModule } from '~/infrastructure/novu/novu.module'; /** * Модуль wallet для управления выводом средств, депозитными платежами и генерацией документов */ @Module({ - imports: [DomainModule], - providers: [WalletResolver, WalletService, WalletDomainInteractor], + imports: [DomainModule, NovuModule], + providers: [WalletResolver, WalletService, WalletNotificationService, WalletDomainInteractor], exports: [WalletService, WalletDomainInteractor], }) export class WalletModule {} diff --git a/components/controller/src/domain/desktop/entities/desktop-domain.entity.ts b/components/controller/src/domain/desktop/entities/desktop-domain.entity.ts index 1282dd80b2c..353bd6b02d2 100644 --- a/components/controller/src/domain/desktop/entities/desktop-domain.entity.ts +++ b/components/controller/src/domain/desktop/entities/desktop-domain.entity.ts @@ -1,19 +1,19 @@ import type { DesktopDomainInterface } from '../interfaces/desktop-domain.interface'; import type { DesktopWorkspaceDomainEntity } from './workspace-domain.entity'; +/** + * Доменная сущность Desktop + * Содержит информацию о доступных рабочих столах (workspaces) + * Настройки по умолчанию (authorized/nonAuthorizedHome) перенесены в Settings + */ export class DesktopDomainEntity implements DesktopDomainInterface { public readonly coopname!: string; public readonly layout!: string; - public readonly authorizedHome!: string; - public readonly nonAuthorizedHome!: string; - public readonly workspaces!: DesktopWorkspaceDomainEntity[]; constructor(data: DesktopDomainEntity) { this.coopname = data.coopname; this.layout = data.layout; - this.authorizedHome = data.authorizedHome; - this.nonAuthorizedHome = data.nonAuthorizedHome; this.workspaces = data.workspaces; } } diff --git a/components/controller/src/domain/desktop/interactors/desktop.interactor.ts b/components/controller/src/domain/desktop/interactors/desktop.interactor.ts index 3dcaff9c148..5a586ff974f 100644 --- a/components/controller/src/domain/desktop/interactors/desktop.interactor.ts +++ b/components/controller/src/domain/desktop/interactors/desktop.interactor.ts @@ -40,14 +40,11 @@ export class DesktopDomainInteractor { } const layout = 'default'; - const authorizedHome = 'profile'; - const nonAuthorizedHome = 'signup'; + return new DesktopDomainEntity({ coopname: config.coopname, layout, workspaces, - authorizedHome, - nonAuthorizedHome, }); } } diff --git a/components/controller/src/domain/desktop/interfaces/desktop-domain.interface.ts b/components/controller/src/domain/desktop/interfaces/desktop-domain.interface.ts index f29a31b5381..3a79472e7be 100644 --- a/components/controller/src/domain/desktop/interfaces/desktop-domain.interface.ts +++ b/components/controller/src/domain/desktop/interfaces/desktop-domain.interface.ts @@ -1,9 +1,11 @@ import type { DesktopWorkspaceDomainInterface } from './desktop-workspace-domain.interface'; +/** + * Доменный интерфейс Desktop + * Настройки по умолчанию (authorized/nonAuthorizedHome) перенесены в Settings + */ export interface DesktopDomainInterface { coopname: string; layout: string; - authorizedHome: string; - nonAuthorizedHome: string; workspaces: DesktopWorkspaceDomainInterface[]; } diff --git a/components/controller/src/domain/domain.module.ts b/components/controller/src/domain/domain.module.ts index 639543e1d7b..c92bcaaf936 100644 --- a/components/controller/src/domain/domain.module.ts +++ b/components/controller/src/domain/domain.module.ts @@ -21,6 +21,7 @@ import { WalletDomainModule } from './wallet/wallet-domain.module'; import { NotificationDomainModule } from './notification/notification-domain.module'; import { LedgerDomainModule } from './ledger/ledger-domain.module'; import { ParserDomainModule } from './parser/parser-domain.module'; +import { SettingsDomainModule } from './settings/settings-domain.module'; @Module({ imports: [ @@ -45,6 +46,7 @@ import { ParserDomainModule } from './parser/parser-domain.module'; NotificationDomainModule, LedgerDomainModule, ParserDomainModule, + SettingsDomainModule, ], exports: [ AuthDomainModule, @@ -68,6 +70,7 @@ import { ParserDomainModule } from './parser/parser-domain.module'; NotificationDomainModule, LedgerDomainModule, ParserDomainModule, + SettingsDomainModule, ], }) export class DomainModule {} diff --git a/components/controller/src/domain/notification/interfaces/novu-workflow.port.ts b/components/controller/src/domain/notification/interfaces/novu-workflow.port.ts index 2ed6a63ebe5..e433500d206 100644 --- a/components/controller/src/domain/notification/interfaces/novu-workflow.port.ts +++ b/components/controller/src/domain/notification/interfaces/novu-workflow.port.ts @@ -1,7 +1,6 @@ import type { WorkflowTriggerDomainInterface, WorkflowTriggerResultDomainInterface, - WorkflowBulkTriggerDomainInterface, } from './workflow-trigger-domain.interface'; /** @@ -16,13 +15,6 @@ export interface NovuWorkflowPort { */ triggerWorkflow(triggerData: WorkflowTriggerDomainInterface): Promise; - /** - * Запустить воркфлоу для множественной отправки уведомлений - * @param bulkTriggerData Данные для пакетного запуска воркфлоу - * @returns Promise - */ - triggerBulkWorkflow(bulkTriggerData: WorkflowBulkTriggerDomainInterface): Promise; - /** * Отменить триггер воркфлоу * @param transactionId ID транзакции для отмены diff --git a/components/controller/src/domain/notification/interfaces/workflow-trigger-domain.interface.ts b/components/controller/src/domain/notification/interfaces/workflow-trigger-domain.interface.ts index 988d83348a5..aea0d593ffc 100644 --- a/components/controller/src/domain/notification/interfaces/workflow-trigger-domain.interface.ts +++ b/components/controller/src/domain/notification/interfaces/workflow-trigger-domain.interface.ts @@ -118,38 +118,3 @@ export interface WorkflowTriggerResultDomainInterface { */ error?: string[]; } - -/** - * Доменный интерфейс для пакетного триггера воркфлоу - */ -export interface WorkflowBulkTriggerDomainInterface { - /** - * Имя воркфлоу - */ - name: string; - - /** - * Массив событий для пакетной отправки - */ - events: WorkflowBulkEventDomainInterface[]; -} - -/** - * Доменный интерфейс для события в пакетном триггере - */ -export interface WorkflowBulkEventDomainInterface { - /** - * Данные для воркфлоу (payload) - */ - to: WorkflowRecipientDomainInterface | WorkflowRecipientDomainInterface[]; - - /** - * Данные для шаблона уведомления - */ - payload?: Record; - - /** - * Дополнительные параметры - */ - overrides?: Record; -} diff --git a/components/controller/src/domain/settings/entities/settings-domain.entity.ts b/components/controller/src/domain/settings/entities/settings-domain.entity.ts new file mode 100644 index 00000000000..797bc0b8689 --- /dev/null +++ b/components/controller/src/domain/settings/entities/settings-domain.entity.ts @@ -0,0 +1,63 @@ +import type { SettingsDomainInterface } from '../interfaces/settings-domain.interface'; + +/** + * Доменная сущность настроек системы + * Инкапсулирует бизнес-логику работы с настройками + */ +export class SettingsDomainEntity implements SettingsDomainInterface { + coopname: string; + authorized_default_workspace: string; + authorized_default_route: string; + non_authorized_default_workspace: string; + non_authorized_default_route: string; + extra_settings?: Record; + created_at: Date; + updated_at: Date; + + constructor(data: SettingsDomainInterface) { + this.coopname = data.coopname; + this.authorized_default_workspace = data.authorized_default_workspace; + this.authorized_default_route = data.authorized_default_route; + this.non_authorized_default_workspace = data.non_authorized_default_workspace; + this.non_authorized_default_route = data.non_authorized_default_route; + this.created_at = data.created_at; + this.updated_at = data.updated_at; + } + + /** + * Обновляет настройки частично + * @param updates - объект с полями для обновления + */ + update(updates: Partial>): void { + if (updates.authorized_default_workspace !== undefined) { + this.authorized_default_workspace = updates.authorized_default_workspace; + } + if (updates.authorized_default_route !== undefined) { + this.authorized_default_route = updates.authorized_default_route; + } + if (updates.non_authorized_default_workspace !== undefined) { + this.non_authorized_default_workspace = updates.non_authorized_default_workspace; + } + if (updates.non_authorized_default_route !== undefined) { + this.non_authorized_default_route = updates.non_authorized_default_route; + } + this.updated_at = new Date(); + } + + /** + * Создает дефолтные настройки для кооператива + * @param coopname - название кооператива + */ + static createDefault(coopname: string): SettingsDomainEntity { + const now = new Date(); + return new SettingsDomainEntity({ + coopname, + authorized_default_workspace: 'participant', + authorized_default_route: 'wallet', + non_authorized_default_workspace: 'participant', + non_authorized_default_route: 'signup', + created_at: now, + updated_at: now, + }); + } +} diff --git a/components/controller/src/domain/settings/interactors/settings.interactor.ts b/components/controller/src/domain/settings/interactors/settings.interactor.ts new file mode 100644 index 00000000000..807fec0d703 --- /dev/null +++ b/components/controller/src/domain/settings/interactors/settings.interactor.ts @@ -0,0 +1,48 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { SETTINGS_REPOSITORY, SettingsRepository } from '../repositories/settings.repository'; +import { SettingsDomainEntity } from '../entities/settings-domain.entity'; +import type { UpdateSettingsInputDomainInterface } from '../interfaces/update-settings-input-domain.interface'; +import config from '~/config/config'; + +/** + * Интерактор для работы с настройками системы + * Реализует бизнес-логику управления настройками + */ +@Injectable() +export class SettingsDomainInteractor { + constructor(@Inject(SETTINGS_REPOSITORY) private readonly settingsRepository: SettingsRepository) {} + + /** + * Получает настройки кооператива + * Если настройки не найдены, создает дефолтные + */ + async getSettings(): Promise { + const settings = await this.settingsRepository.get(config.coopname); + + if (!settings) { + // Если настроек нет, создаем дефолтные + const defaultSettings = SettingsDomainEntity.createDefault(config.coopname); + const created = await this.settingsRepository.upsert(defaultSettings); + return new SettingsDomainEntity(created); + } + + return new SettingsDomainEntity(settings); + } + + /** + * Обновляет настройки кооператива + * @param updates - объект с полями для обновления + */ + async updateSettings(updates: UpdateSettingsInputDomainInterface): Promise { + // Получаем текущие настройки или создаем дефолтные + const currentSettings = await this.getSettings(); + + // Обновляем настройки + currentSettings.update(updates); + + // Сохраняем обновленные настройки + const updated = await this.settingsRepository.upsert(currentSettings); + + return new SettingsDomainEntity(updated); + } +} diff --git a/components/controller/src/domain/settings/interfaces/settings-domain.interface.ts b/components/controller/src/domain/settings/interfaces/settings-domain.interface.ts new file mode 100644 index 00000000000..4ee061b8301 --- /dev/null +++ b/components/controller/src/domain/settings/interfaces/settings-domain.interface.ts @@ -0,0 +1,44 @@ +/** + * Доменный интерфейс для настроек системы + * Содержит конфигурацию рабочих столов и маршрутов по умолчанию + */ +export interface SettingsDomainInterface { + /** + * Название кооператива, к которому относятся настройки + */ + coopname: string; + + /** + * Рабочий стол по умолчанию для авторизованных пользователей + * Например: 'participant', 'soviet', 'chairman' + */ + authorized_default_workspace: string; + + /** + * Маршрут по умолчанию для авторизованных пользователей + * Например: 'wallet', 'agenda', 'profile' + */ + authorized_default_route: string; + + /** + * Рабочий стол по умолчанию для неавторизованных пользователей + * Обычно 'participant' или пустой + */ + non_authorized_default_workspace: string; + + /** + * Маршрут по умолчанию для неавторизованных пользователей + * Обычно 'signup' или 'signin' + */ + non_authorized_default_route: string; + + /** + * Дата создания настроек + */ + created_at: Date; + + /** + * Дата последнего обновления настроек + */ + updated_at: Date; +} diff --git a/components/controller/src/domain/settings/interfaces/update-settings-input-domain.interface.ts b/components/controller/src/domain/settings/interfaces/update-settings-input-domain.interface.ts new file mode 100644 index 00000000000..8841276ae04 --- /dev/null +++ b/components/controller/src/domain/settings/interfaces/update-settings-input-domain.interface.ts @@ -0,0 +1,25 @@ +/** + * Доменный интерфейс для обновления настроек системы + * Все поля опциональны для частичного обновления + */ +export interface UpdateSettingsInputDomainInterface { + /** + * Рабочий стол по умолчанию для авторизованных пользователей + */ + authorized_default_workspace?: string; + + /** + * Маршрут по умолчанию для авторизованных пользователей + */ + authorized_default_route?: string; + + /** + * Рабочий стол по умолчанию для неавторизованных пользователей + */ + non_authorized_default_workspace?: string; + + /** + * Маршрут по умолчанию для неавторизованных пользователей + */ + non_authorized_default_route?: string; +} diff --git a/components/controller/src/domain/settings/repositories/settings.repository.ts b/components/controller/src/domain/settings/repositories/settings.repository.ts new file mode 100644 index 00000000000..eeb36562a5b --- /dev/null +++ b/components/controller/src/domain/settings/repositories/settings.repository.ts @@ -0,0 +1,33 @@ +import type { SettingsDomainInterface } from '../interfaces/settings-domain.interface'; +import type { UpdateSettingsInputDomainInterface } from '../interfaces/update-settings-input-domain.interface'; + +/** + * Репозиторий для работы с настройками системы + * Определяет контракт для работы с хранилищем настроек + */ +export interface SettingsRepository { + /** + * Получает настройки кооператива + * @param coopname - название кооператива + * @returns настройки или null, если не найдены + */ + get(coopname: string): Promise; + + /** + * Создает или обновляет настройки кооператива + * @param settings - объект настроек + */ + upsert(settings: SettingsDomainInterface): Promise; + + /** + * Обновляет настройки кооператива частично + * @param coopname - название кооператива + * @param updates - объект с полями для обновления + */ + update(coopname: string, updates: UpdateSettingsInputDomainInterface): Promise; +} + +/** + * Токен для инъекции зависимости репозитория настроек + */ +export const SETTINGS_REPOSITORY = Symbol('SettingsRepository'); diff --git a/components/controller/src/domain/settings/settings-domain.module.ts b/components/controller/src/domain/settings/settings-domain.module.ts new file mode 100644 index 00000000000..93025f6569c --- /dev/null +++ b/components/controller/src/domain/settings/settings-domain.module.ts @@ -0,0 +1,11 @@ +import { Module } from '@nestjs/common'; +import { SettingsDomainInteractor } from './interactors/settings.interactor'; + +/** + * Модуль доменного слоя для работы с настройками + */ +@Module({ + providers: [SettingsDomainInteractor], + exports: [SettingsDomainInteractor], +}) +export class SettingsDomainModule {} diff --git a/components/controller/src/domain/settings/settings.module.ts b/components/controller/src/domain/settings/settings.module.ts new file mode 100644 index 00000000000..5fe16157696 --- /dev/null +++ b/components/controller/src/domain/settings/settings.module.ts @@ -0,0 +1,12 @@ +import { Module } from '@nestjs/common'; +import { SettingsDomainModule } from './settings-domain.module'; + +/** + * Модуль настроек, объединяющий домен и инфраструктуру + * Импортирует SettingsDomainModule и предоставляет репозиторий через TypeOrmModule + */ +@Module({ + imports: [SettingsDomainModule], + exports: [SettingsDomainModule], +}) +export class SettingsModule {} diff --git a/components/controller/src/domain/system/entities/systeminfo-domain.entity.ts b/components/controller/src/domain/system/entities/systeminfo-domain.entity.ts index 86e144349fa..1843a256dd4 100644 --- a/components/controller/src/domain/system/entities/systeminfo-domain.entity.ts +++ b/components/controller/src/domain/system/entities/systeminfo-domain.entity.ts @@ -5,6 +5,7 @@ import type { GetInfoResult } from '~/types/shared/blockchain.types'; import { CooperativeContactsDomainInterface } from '../interfaces/cooperative-contacts-domain.interface'; import type { VarsDomainInterface } from '../interfaces/vars-domain.interface'; import { SymbolsDTO } from '~/application/system/dto/symbols.dto'; +import type { SettingsDomainInterface } from '~/domain/settings/interfaces/settings-domain.interface'; export class SystemInfoDomainEntity { public readonly coopname!: string; @@ -16,6 +17,7 @@ export class SystemInfoDomainEntity { public readonly contacts?: CooperativeContactsDomainInterface; public readonly vars!: VarsDomainInterface | null; public readonly symbols!: SymbolsDTO; + public readonly settings!: SettingsDomainInterface; constructor(data: SystemInfoDomainEntity) { Object.assign(this, data); diff --git a/components/controller/src/domain/system/interactors/system.interactor.ts b/components/controller/src/domain/system/interactors/system.interactor.ts index 3cf16916acf..e5917970d26 100644 --- a/components/controller/src/domain/system/interactors/system.interactor.ts +++ b/components/controller/src/domain/system/interactors/system.interactor.ts @@ -14,6 +14,9 @@ import { VARS_REPOSITORY, VarsRepository } from '~/domain/common/repositories/va import type { UpdateInputDomainInterface } from '../interfaces/update-input-domain.interface'; import { ORGANIZATION_REPOSITORY, type OrganizationRepository } from '~/domain/common/repositories/organization.repository'; import { SymbolsDTO } from '~/application/system/dto/symbols.dto'; +import { SettingsDomainInteractor } from '~/domain/settings/interactors/settings.interactor'; +import type { UpdateSettingsInputDomainInterface } from '~/domain/settings/interfaces/update-settings-input-domain.interface'; +import type { SettingsDomainEntity } from '~/domain/settings/entities/settings-domain.entity'; @Injectable() export class SystemDomainInteractor { @@ -22,7 +25,8 @@ export class SystemDomainInteractor { @Inject(SYSTEM_BLOCKCHAIN_PORT) private readonly systemBlockchainPort: SystemBlockchainPort, private readonly systemDomainService: SystemDomainService, @Inject(VARS_REPOSITORY) private readonly varsRepository: VarsRepository, - @Inject(ORGANIZATION_REPOSITORY) private readonly organizationRepository: OrganizationRepository + @Inject(ORGANIZATION_REPOSITORY) private readonly organizationRepository: OrganizationRepository, + private readonly settingsDomainInteractor: SettingsDomainInteractor ) {} async setWif(data: SetWifInputDomainInterface): Promise { @@ -79,6 +83,9 @@ export class SystemDomainInteractor { config.blockchain.root_govern_precision ); + // Получаем настройки системы + const settings = await this.getSettings(); + return new SystemInfoDomainEntity({ blockchain_info, contacts, @@ -89,6 +96,22 @@ export class SystemDomainInteractor { blockchain_account, system_status, symbols, + settings, }); } + + /** + * Получает настройки системы + */ + async getSettings(): Promise { + return this.settingsDomainInteractor.getSettings(); + } + + /** + * Обновляет настройки системы + * @param updates - объект с полями для обновления + */ + async updateSettings(updates: UpdateSettingsInputDomainInterface): Promise { + return this.settingsDomainInteractor.updateSettings(updates); + } } diff --git a/components/controller/src/domain/system/system-domain.module.ts b/components/controller/src/domain/system/system-domain.module.ts index 4708b54b27f..6e9aa86a49e 100644 --- a/components/controller/src/domain/system/system-domain.module.ts +++ b/components/controller/src/domain/system/system-domain.module.ts @@ -3,9 +3,10 @@ import { Module } from '@nestjs/common'; import { SystemDomainInteractor } from './interactors/system.interactor'; import { AccountDomainModule } from '../account/account-domain.module'; import { SystemDomainService } from './services/system-domain.service'; +import { SettingsModule } from '../settings/settings.module'; @Module({ - imports: [AccountDomainModule], + imports: [AccountDomainModule, SettingsModule], providers: [SystemDomainService, SystemDomainInteractor], exports: [SystemDomainService, SystemDomainInteractor], }) diff --git a/components/controller/src/extensions/capital/application/dto/project_management/project.dto.ts b/components/controller/src/extensions/capital/application/dto/project_management/project.dto.ts index f48139ab2fa..c3b6025909b 100644 --- a/components/controller/src/extensions/capital/application/dto/project_management/project.dto.ts +++ b/components/controller/src/extensions/capital/application/dto/project_management/project.dto.ts @@ -252,7 +252,7 @@ export class CapitalProjectFactPoolOutputDTO { total_generation_pool!: string; @Field(() => String, { - description: 'Общий объем вклада', + description: 'Общий объем взноса старших участников', }) total_contribution!: string; diff --git a/components/controller/src/extensions/capital/application/dto/segments/segment.dto.ts b/components/controller/src/extensions/capital/application/dto/segments/segment.dto.ts index 481c492fc3a..1d2e641654b 100644 --- a/components/controller/src/extensions/capital/application/dto/segments/segment.dto.ts +++ b/components/controller/src/extensions/capital/application/dto/segments/segment.dto.ts @@ -88,6 +88,12 @@ export class SegmentOutputDTO extends BaseOutputDTO { }) investor_base!: string; + @Field(() => String, { + nullable: true, + description: 'Вклад участника словами участника', + }) + value?: string; + // Вклады создателя @Field(() => String, { description: 'Базовый вклад создателя', diff --git a/components/controller/src/extensions/capital/infrastructure/mappers/segment.mapper.ts b/components/controller/src/extensions/capital/infrastructure/mappers/segment.mapper.ts index 6484c55871f..2d3efaefed4 100644 --- a/components/controller/src/extensions/capital/infrastructure/mappers/segment.mapper.ts +++ b/components/controller/src/extensions/capital/infrastructure/mappers/segment.mapper.ts @@ -10,6 +10,8 @@ import { ResultOutputDTO } from '../../application/dto/result_submission/result. import { DocumentAggregationService } from '~/domain/document/services/document-aggregation.service'; import { ContributorRepository } from '../../domain/repositories/contributor.repository'; import { CONTRIBUTOR_REPOSITORY } from '../../domain/repositories/contributor.repository'; +import { AppendixRepository } from '../../domain/repositories/appendix.repository'; +import { APPENDIX_REPOSITORY } from '../../domain/repositories/appendix.repository'; import { Inject } from '@nestjs/common'; import { DocumentAggregateDTO } from '~/application/document/dto/document-aggregate.dto'; @@ -26,7 +28,9 @@ export class SegmentMapper { constructor( private readonly documentAggregationService: DocumentAggregationService, @Inject(CONTRIBUTOR_REPOSITORY) - private readonly contributorRepository: ContributorRepository + private readonly contributorRepository: ContributorRepository, + @Inject(APPENDIX_REPOSITORY) + private readonly appendixRepository: AppendixRepository ) {} /** * Преобразование TypeORM сущности в доменную сущность @@ -222,6 +226,21 @@ export class SegmentMapper { displayName = ''; } + // Получаем contribution из appendix + let contribution: string | undefined = undefined; + try { + if (domain.username && domain.project_hash) { + const appendix = await this.appendixRepository.findConfirmedByUsernameAndProjectHash( + domain.username, + domain.project_hash + ); + contribution = appendix?.contribution; + } + } catch (error) { + // Если не удалось получить appendix, оставляем undefined + contribution = undefined; + } + // Обогащаем документы в result let enrichedResult: ResultOutputDTO | undefined = undefined; if (domain.result) { @@ -251,6 +270,7 @@ export class SegmentMapper { return { ...domain, display_name: displayName, + value: contribution, result: enrichedResult, } as SegmentOutputDTO; } diff --git a/components/controller/src/extensions/chairman/application/services/approval-response-notification.service.ts b/components/controller/src/extensions/chairman/application/services/approval-response-notification.service.ts index 5402fad8624..41d9bc9995f 100644 --- a/components/controller/src/extensions/chairman/application/services/approval-response-notification.service.ts +++ b/components/controller/src/extensions/chairman/application/services/approval-response-notification.service.ts @@ -10,6 +10,7 @@ import type { WorkflowTriggerDomainInterface } from '~/domain/notification/inter import { Workflows } from '@coopenomics/notifications'; import { SovietContract } from 'cooptypes'; import { ApprovalRepository, APPROVAL_REPOSITORY } from '../../domain/repositories/approval.repository'; +import { ApprovalStatus } from '../../domain'; /** * Сервис для отправки уведомлений об ответах на запросы одобрения @@ -114,6 +115,7 @@ export class ApprovalResponseNotificationService implements OnModuleInit { const payload: Workflows.ApprovalResponse.IPayload = { userName: authorName, approvalStatus: status, + approvalStatusText: status === ApprovalStatus.APPROVED ? 'одобрен' : 'отклонён', approvalId: approvalHash, coopname: config.coopname, coopShortName, diff --git a/components/controller/src/extensions/participant/meet-workflow-notification.service.ts b/components/controller/src/extensions/participant/meet-workflow-notification.service.ts index cc0fcdc8bd9..4bc48a4820d 100644 --- a/components/controller/src/extensions/participant/meet-workflow-notification.service.ts +++ b/components/controller/src/extensions/participant/meet-workflow-notification.service.ts @@ -7,10 +7,7 @@ import { DateUtils } from '~/shared/utils/date-utils'; import { ExtendedMeetStatus } from '~/domain/meet/enums/extended-meet-status.enum'; import type { TrackedMeet } from './types'; import { ACCOUNT_EXTENSION_PORT, AccountExtensionPort } from '~/domain/extension/ports/account-extension-port'; -import type { - WorkflowBulkTriggerDomainInterface, - WorkflowBulkEventDomainInterface, -} from '~/domain/notification/interfaces/workflow-trigger-domain.interface'; +import type { WorkflowTriggerDomainInterface } from '~/domain/notification/interfaces/workflow-trigger-domain.interface'; import { Workflows } from '@coopenomics/notifications'; /** @@ -127,23 +124,28 @@ export class MeetWorkflowNotificationService implements OnModuleInit { meetUrl, }; - const events: WorkflowBulkEventDomainInterface[] = users.map((user) => ({ - to: { - subscriberId: user.username, - email: user.email, - }, - payload, - })); + // Отправляем уведомления каждому пользователю в цикле + let sentCount = 0; + for (const user of users) { + const triggerData: WorkflowTriggerDomainInterface = { + name: Workflows.MeetInitial.id, + to: { + subscriberId: user.username, + email: user.email, + }, + payload, + }; - const bulkTriggerData: WorkflowBulkTriggerDomainInterface = { - name: Workflows.MeetInitial.id, - events, - }; - - await this.novuWorkflowAdapter.triggerBulkWorkflow(bulkTriggerData); + try { + await this.novuWorkflowAdapter.triggerWorkflow(triggerData); + sentCount++; + } catch (error: any) { + this.logger.error(`Ошибка отправки начального уведомления пользователю ${user.username}: ${error.message}`); + } + } this.logger.info( - `Отправлено начальное уведомление о собрании ${meet.hash} (№${meet.id}) для ${users.length} пользователей` + `Отправлено начальное уведомление о собрании ${meet.hash} (№${meet.id}) для ${sentCount}/${users.length} пользователей` ); } @@ -173,23 +175,28 @@ export class MeetWorkflowNotificationService implements OnModuleInit { meetUrl, }; - const events: WorkflowBulkEventDomainInterface[] = users.map((user) => ({ - to: { - subscriberId: user.username, - email: user.email, - }, - payload, - })); + // Отправляем уведомления каждому пользователю в цикле + let sentCount = 0; + for (const user of users) { + const triggerData: WorkflowTriggerDomainInterface = { + name: Workflows.MeetReminderStart.id, + to: { + subscriberId: user.username, + email: user.email, + }, + payload, + }; - const bulkTriggerData: WorkflowBulkTriggerDomainInterface = { - name: Workflows.MeetReminderStart.id, - events, - }; - - await this.novuWorkflowAdapter.triggerBulkWorkflow(bulkTriggerData); + try { + await this.novuWorkflowAdapter.triggerWorkflow(triggerData); + sentCount++; + } catch (error: any) { + this.logger.error(`Ошибка отправки уведомления о начале собрания пользователю ${user.username}: ${error.message}`); + } + } this.logger.info( - `Отправлено уведомление за ${timeDescription} до начала собрания ${meet.hash} (№${meet.id}) для ${users.length} пользователей` + `Отправлено уведомление за ${timeDescription} до начала собрания ${meet.hash} (№${meet.id}) для ${sentCount}/${users.length} пользователей` ); } @@ -213,23 +220,28 @@ export class MeetWorkflowNotificationService implements OnModuleInit { meetUrl, }; - const events: WorkflowBulkEventDomainInterface[] = users.map((user) => ({ - to: { - subscriberId: user.username, - email: user.email, - }, - payload, - })); + // Отправляем уведомления каждому пользователю в цикле + let sentCount = 0; + for (const user of users) { + const triggerData: WorkflowTriggerDomainInterface = { + name: Workflows.MeetStarted.id, + to: { + subscriberId: user.username, + email: user.email, + }, + payload, + }; - const bulkTriggerData: WorkflowBulkTriggerDomainInterface = { - name: Workflows.MeetStarted.id, - events, - }; - - await this.novuWorkflowAdapter.triggerBulkWorkflow(bulkTriggerData); + try { + await this.novuWorkflowAdapter.triggerWorkflow(triggerData); + sentCount++; + } catch (error: any) { + this.logger.error(`Ошибка отправки уведомления о старте собрания пользователю ${user.username}: ${error.message}`); + } + } this.logger.info( - `Отправлено уведомление о начале собрания ${meet.hash} (№${meet.id}) для ${users.length} пользователей` + `Отправлено уведомление о начале собрания ${meet.hash} (№${meet.id}) для ${sentCount}/${users.length} пользователей` ); } @@ -261,23 +273,30 @@ export class MeetWorkflowNotificationService implements OnModuleInit { meetUrl, }; - const events: WorkflowBulkEventDomainInterface[] = users.map((user) => ({ - to: { - subscriberId: user.username, - email: user.email, - }, - payload, - })); + // Отправляем уведомления каждому пользователю в цикле + let sentCount = 0; + for (const user of users) { + const triggerData: WorkflowTriggerDomainInterface = { + name: Workflows.MeetReminderEnd.id, + to: { + subscriberId: user.username, + email: user.email, + }, + payload, + }; - const bulkTriggerData: WorkflowBulkTriggerDomainInterface = { - name: Workflows.MeetReminderEnd.id, - events, - }; - - await this.novuWorkflowAdapter.triggerBulkWorkflow(bulkTriggerData); + try { + await this.novuWorkflowAdapter.triggerWorkflow(triggerData); + sentCount++; + } catch (error: any) { + this.logger.error( + `Ошибка отправки уведомления об окончании собрания пользователю ${user.username}: ${error.message}` + ); + } + } this.logger.info( - `Отправлено уведомление за ${timeDescription} до завершения собрания ${meet.hash} (№${meet.id}) для ${users.length} пользователей` + `Отправлено уведомление за ${timeDescription} до завершения собрания ${meet.hash} (№${meet.id}) для ${sentCount}/${users.length} пользователей` ); } @@ -305,23 +324,30 @@ export class MeetWorkflowNotificationService implements OnModuleInit { meetUrl, }; - const events: WorkflowBulkEventDomainInterface[] = users.map((user) => ({ - to: { - subscriberId: user.username, - email: user.email, - }, - payload, - })); + // Отправляем уведомления каждому пользователю в цикле + let sentCount = 0; + for (const user of users) { + const triggerData: WorkflowTriggerDomainInterface = { + name: Workflows.MeetRestart.id, + to: { + subscriberId: user.username, + email: user.email, + }, + payload, + }; - const bulkTriggerData: WorkflowBulkTriggerDomainInterface = { - name: Workflows.MeetRestart.id, - events, - }; - - await this.novuWorkflowAdapter.triggerBulkWorkflow(bulkTriggerData); + try { + await this.novuWorkflowAdapter.triggerWorkflow(triggerData); + sentCount++; + } catch (error: any) { + this.logger.error( + `Ошибка отправки уведомления о повторном собрании пользователю ${user.username}: ${error.message}` + ); + } + } this.logger.info( - `Отправлено уведомление о новой дате повторного собрания ${meet.hash} (№${meet.id}) для ${users.length} пользователей` + `Отправлено уведомление о новой дате повторного собрания ${meet.hash} (№${meet.id}) для ${sentCount}/${users.length} пользователей` ); } @@ -371,23 +397,30 @@ export class MeetWorkflowNotificationService implements OnModuleInit { endMessage, }; - const events: WorkflowBulkEventDomainInterface[] = users.map((user) => ({ - to: { - subscriberId: user.username, - email: user.email, - }, - payload, - })); + // Отправляем уведомления каждому пользователю в цикле + let sentCount = 0; + for (const user of users) { + const triggerData: WorkflowTriggerDomainInterface = { + name: Workflows.MeetEnded.id, + to: { + subscriberId: user.username, + email: user.email, + }, + payload, + }; - const bulkTriggerData: WorkflowBulkTriggerDomainInterface = { - name: Workflows.MeetEnded.id, - events, - }; - - await this.novuWorkflowAdapter.triggerBulkWorkflow(bulkTriggerData); + try { + await this.novuWorkflowAdapter.triggerWorkflow(triggerData); + sentCount++; + } catch (error: any) { + this.logger.error( + `Ошибка отправки уведомления о завершении собрания пользователю ${user.username}: ${error.message}` + ); + } + } this.logger.info( - `Отправлено уведомление о завершении собрания ${meet.hash} (№${meet.id}) для ${users.length} пользователей` + `Отправлено уведомление о завершении собрания ${meet.hash} (№${meet.id}) для ${sentCount}/${users.length} пользователей` ); } } diff --git a/components/controller/src/infrastructure/blockchain/blockchain-consumer.service.ts b/components/controller/src/infrastructure/blockchain/blockchain-consumer.service.ts index 3dba6054450..b2a985c9ab6 100644 --- a/components/controller/src/infrastructure/blockchain/blockchain-consumer.service.ts +++ b/components/controller/src/infrastructure/blockchain/blockchain-consumer.service.ts @@ -103,6 +103,9 @@ export class BlockchainConsumerService implements OnModuleInit, OnModuleDestroy * Выполняет минимальную предварительную фильтрацию, сохраняет в базу и публикует событие во внутреннюю шину */ private async processAction(action: IAction): Promise { + if (action.receiver != action.account) { + return; + } this.logger.debug(`Обработка действия: ${action.name} от ${action.account}: ${JSON.stringify(action.data)}`); // Откладываем обработку на 3 секунды асинхронно diff --git a/components/controller/src/infrastructure/database/typeorm/entities/settings.entity.ts b/components/controller/src/infrastructure/database/typeorm/entities/settings.entity.ts new file mode 100644 index 00000000000..8b94c45564f --- /dev/null +++ b/components/controller/src/infrastructure/database/typeorm/entities/settings.entity.ts @@ -0,0 +1,69 @@ +import { BeforeInsert, BeforeUpdate, Column, CreateDateColumn, Entity, PrimaryColumn, UpdateDateColumn } from 'typeorm'; +import { SettingsDomainEntity } from '~/domain/settings/entities/settings-domain.entity'; +import type { SettingsDomainInterface } from '~/domain/settings/interfaces/settings-domain.interface'; + +/** + * TypeORM сущность для настроек системы + * Хранит конфигурацию рабочих столов и маршрутов по умолчанию + */ +@Entity('settings') +export class SettingsEntity implements SettingsDomainInterface { + @PrimaryColumn({ unique: true, length: 12 }) + coopname!: string; + + @Column({ length: 50 }) + authorized_default_workspace!: string; + + @Column({ length: 50 }) + authorized_default_route!: string; + + @Column({ length: 50 }) + non_authorized_default_workspace!: string; + + @Column({ length: 50 }) + non_authorized_default_route!: string; + + @Column('jsonb', { default: {} }) + extra_settings?: Record; + + @CreateDateColumn() + created_at!: Date; + + @UpdateDateColumn() + updated_at!: Date; + + @BeforeInsert() + @BeforeUpdate() + setDefaultExtraSettings() { + if (this.extra_settings === null || this.extra_settings === undefined) { + this.extra_settings = {}; + } + } + + constructor(data?: SettingsDomainEntity) { + if (data) { + this.coopname = data.coopname; + this.authorized_default_workspace = data.authorized_default_workspace; + this.authorized_default_route = data.authorized_default_route; + this.non_authorized_default_workspace = data.non_authorized_default_workspace; + this.non_authorized_default_route = data.non_authorized_default_route; + this.extra_settings = data.extra_settings; + this.created_at = data.created_at; + this.updated_at = data.updated_at; + } + } + + /** + * Преобразует ORM-сущность в доменную сущность + */ + toDomainEntity(): SettingsDomainEntity { + return new SettingsDomainEntity(this); + } + + /** + * Создает ORM-сущность из доменной сущности + */ + static fromDomainEntity(domainEntity: SettingsDomainEntity): SettingsEntity { + return new SettingsEntity(domainEntity); + } +} diff --git a/components/controller/src/infrastructure/database/typeorm/repositories/settings.typeorm-repository.ts b/components/controller/src/infrastructure/database/typeorm/repositories/settings.typeorm-repository.ts new file mode 100644 index 00000000000..c924d995c8e --- /dev/null +++ b/components/controller/src/infrastructure/database/typeorm/repositories/settings.typeorm-repository.ts @@ -0,0 +1,56 @@ +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { SettingsEntity } from '../entities/settings.entity'; +import type { SettingsRepository } from '~/domain/settings/repositories/settings.repository'; +import type { SettingsDomainInterface } from '~/domain/settings/interfaces/settings-domain.interface'; +import type { UpdateSettingsInputDomainInterface } from '~/domain/settings/interfaces/update-settings-input-domain.interface'; + +/** + * Реализация репозитория настроек на базе TypeORM + * Адаптер между доменным слоем и инфраструктурой базы данных + */ +@Injectable() +export class SettingsTypeormRepository implements SettingsRepository { + constructor( + @InjectRepository(SettingsEntity) + private readonly repository: Repository + ) {} + + /** + * Получает настройки кооператива + */ + async get(coopname: string): Promise { + const entity = await this.repository.findOne({ where: { coopname } }); + return entity ? entity.toDomainEntity() : null; + } + + /** + * Создает или обновляет настройки кооператива + */ + async upsert(settings: SettingsDomainInterface): Promise { + const entity = SettingsEntity.fromDomainEntity(settings as any); + const saved = await this.repository.save(entity); + return saved.toDomainEntity(); + } + + /** + * Обновляет настройки кооператива частично + */ + async update(coopname: string, updates: UpdateSettingsInputDomainInterface): Promise { + // Получаем текущие настройки + const existing = await this.get(coopname); + if (!existing) { + throw new Error(`Settings not found for coopname: ${coopname}`); + } + + // Обновляем только переданные поля + const updated = { + ...existing, + ...updates, + updated_at: new Date(), + }; + + return this.upsert(updated); + } +} diff --git a/components/controller/src/infrastructure/database/typeorm/typeorm.module.ts b/components/controller/src/infrastructure/database/typeorm/typeorm.module.ts index 0ba12e645d9..3698b6c969f 100644 --- a/components/controller/src/infrastructure/database/typeorm/typeorm.module.ts +++ b/components/controller/src/infrastructure/database/typeorm/typeorm.module.ts @@ -50,6 +50,9 @@ import { TypeOrmActionRepository } from './repositories/typeorm-action.repositor import { TypeOrmDeltaRepository } from './repositories/typeorm-delta.repository'; import { TypeOrmForkRepository } from './repositories/typeorm-fork.repository'; import { TypeOrmSyncStateRepository } from './repositories/typeorm-sync-state.repository'; +import { SettingsEntity } from './entities/settings.entity'; +import { SETTINGS_REPOSITORY } from '~/domain/settings/repositories/settings.repository'; +import { SettingsTypeormRepository } from './repositories/settings.typeorm-repository'; @Global() @Module({ @@ -61,7 +64,7 @@ import { TypeOrmSyncStateRepository } from './repositories/typeorm-sync-state.re username: config.postgres.username, password: config.postgres.password, database: config.postgres.database, - entities: [EntityVersionTypeormEntity, path.join(__dirname, '**/entities/*.entity.{ts,js}')], + entities: [EntityVersionTypeormEntity, path.join(__dirname, '**/entities/*entity.{ts,js}')], synchronize: config.env === 'development', // Используем миграции для production logging: false, }), @@ -81,6 +84,7 @@ import { TypeOrmSyncStateRepository } from './repositories/typeorm-sync-state.re ForkEntity, SyncStateEntity, EntityVersionTypeormEntity, + SettingsEntity, ]), ], providers: [ @@ -144,6 +148,10 @@ import { TypeOrmSyncStateRepository } from './repositories/typeorm-sync-state.re provide: SYNC_STATE_REPOSITORY_PORT, useClass: TypeOrmSyncStateRepository, }, + { + provide: SETTINGS_REPOSITORY, + useClass: SettingsTypeormRepository, + }, EntityVersionRepository, EntityVersioningService, ], @@ -164,6 +172,7 @@ import { TypeOrmSyncStateRepository } from './repositories/typeorm-sync-state.re DELTA_REPOSITORY_PORT, FORK_REPOSITORY_PORT, SYNC_STATE_REPOSITORY_PORT, + SETTINGS_REPOSITORY, EntityVersionRepository, EntityVersioningService, ], diff --git a/components/controller/src/infrastructure/novu/novu-workflow.adapter.ts b/components/controller/src/infrastructure/novu/novu-workflow.adapter.ts index 2a9d6007006..45961430e2a 100644 --- a/components/controller/src/infrastructure/novu/novu-workflow.adapter.ts +++ b/components/controller/src/infrastructure/novu/novu-workflow.adapter.ts @@ -5,7 +5,6 @@ import type { NovuWorkflowPort } from '~/domain/notification/interfaces/novu-wor import type { WorkflowTriggerDomainInterface, WorkflowTriggerResultDomainInterface, - WorkflowBulkTriggerDomainInterface, } from '~/domain/notification/interfaces/workflow-trigger-domain.interface'; /** @@ -40,7 +39,13 @@ export class NovuWorkflowAdapter implements NovuWorkflowPort { this.logger.log(`Запуск воркфлоу: ${triggerData.name}`); try { - const response: AxiosResponse = await axios.post(`${this.novuBaseUrl}/events/trigger`, triggerData, { + // Санитизируем payload для предотвращения проблем с кавычками в NOVU + const sanitizedTriggerData = { + ...triggerData, + payload: this.sanitizePayload(triggerData.payload), + }; + + const response: AxiosResponse = await axios.post(`${this.novuBaseUrl}/events/trigger`, sanitizedTriggerData, { headers: this.getHeaders(), }); @@ -48,32 +53,10 @@ export class NovuWorkflowAdapter implements NovuWorkflowPort { return this.mapToTriggerResult(response.data); } catch (error: any) { - this.logger.error(`Ошибка запуска воркфлоу ${triggerData.name}: ${error.message}`, error.stack); - throw error; - } - } - - /** - * Запустить воркфлоу для множественной отправки уведомлений - * @param bulkTriggerData Данные для пакетного запуска воркфлоу - * @returns Promise - */ - async triggerBulkWorkflow( - bulkTriggerData: WorkflowBulkTriggerDomainInterface - ): Promise { - this.logger.log(`Запуск пакетного воркфлоу: ${bulkTriggerData.name}, событий: ${bulkTriggerData.events.length}`); - - try { - const response: AxiosResponse = await axios.post(`${this.novuBaseUrl}/events/trigger/bulk`, bulkTriggerData, { - headers: this.getHeaders(), - }); - - this.logger.log(`Пакетный воркфлоу запущен: ${bulkTriggerData.name}`); - console.log('NOVU bulk API response:', JSON.stringify(response.data, null, 2)); - - return response.data.map((item: any) => this.mapToTriggerResult(item)); - } catch (error: any) { - this.logger.error(`Ошибка запуска пакетного воркфлоу ${bulkTriggerData.name}: ${error.message}`, error.stack); + this.logger.error( + `Ошибка запуска воркфлоу ${triggerData.name}: ${error.message} ${error.response?.data}`, + error.stack + ); throw error; } } @@ -119,6 +102,34 @@ export class NovuWorkflowAdapter implements NovuWorkflowPort { } } + /** + * Санитизирует payload для безопасного использования в NOVU + * Заменяет кавычки на HTML entities для предотвращения проблем с шаблонизацией + * @param payload Объект payload для санитизации + * @returns Санитизированный payload + */ + private sanitizePayload(payload: any): any { + if (typeof payload === 'string') { + return payload.replace(/"/g, '"').replace(/'/g, '''); + } + + if (Array.isArray(payload)) { + return payload.map((item) => this.sanitizePayload(item)); + } + + if (payload !== null && typeof payload === 'object') { + const sanitized: any = {}; + for (const key in payload) { + if (Object.prototype.hasOwnProperty.call(payload, key)) { + sanitized[key] = this.sanitizePayload(payload[key]); + } + } + return sanitized; + } + + return payload; + } + /** * Возвращает заголовки для запросов к NOVU API */ diff --git a/components/controller/tsconfig.json b/components/controller/tsconfig.json index ce2c323f0ef..e3db4e19c3e 100644 --- a/components/controller/tsconfig.json +++ b/components/controller/tsconfig.json @@ -12,6 +12,9 @@ "allowJs": false, "noImplicitAny": false, "skipLibCheck": true, + "composite": true, + "incremental": true, + "tsBuildInfoFile": "./dist/.tsbuildinfo", "paths": { "~/*": ["*"] } diff --git a/components/cooptypes/tsconfig.json b/components/cooptypes/tsconfig.json index 7bd70a8b4f8..a1eac59d787 100644 --- a/components/cooptypes/tsconfig.json +++ b/components/cooptypes/tsconfig.json @@ -10,5 +10,6 @@ "esModuleInterop": true, "skipDefaultLibCheck": true, "skipLibCheck": true - } + }, + "include": ["src/**/*"] } diff --git a/components/desktop/extensions/capital/features/Invest/CreateProjectInvest/ui/Dialog/CreateProjectInvestDialog.vue b/components/desktop/extensions/capital/features/Invest/CreateProjectInvest/ui/Dialog/CreateProjectInvestDialog.vue new file mode 100644 index 00000000000..0f535977c39 --- /dev/null +++ b/components/desktop/extensions/capital/features/Invest/CreateProjectInvest/ui/Dialog/CreateProjectInvestDialog.vue @@ -0,0 +1,91 @@ + + + diff --git a/components/desktop/extensions/capital/features/Invest/CreateProjectInvest/ui/Dialog/index.ts b/components/desktop/extensions/capital/features/Invest/CreateProjectInvest/ui/Dialog/index.ts new file mode 100644 index 00000000000..4d631a79aa0 --- /dev/null +++ b/components/desktop/extensions/capital/features/Invest/CreateProjectInvest/ui/Dialog/index.ts @@ -0,0 +1 @@ +export { default as CreateProjectInvestDialog } from './CreateProjectInvestDialog.vue'; diff --git a/components/desktop/extensions/capital/features/Invest/CreateProjectInvest/ui/Fab/ComponentInvestFabAction.vue b/components/desktop/extensions/capital/features/Invest/CreateProjectInvest/ui/Fab/ComponentInvestFabAction.vue new file mode 100644 index 00000000000..4940ac97f40 --- /dev/null +++ b/components/desktop/extensions/capital/features/Invest/CreateProjectInvest/ui/Fab/ComponentInvestFabAction.vue @@ -0,0 +1,43 @@ + + + diff --git a/components/desktop/extensions/capital/features/Invest/CreateProjectInvest/ui/Fab/ProjectInvestFabAction.vue b/components/desktop/extensions/capital/features/Invest/CreateProjectInvest/ui/Fab/ProjectInvestFabAction.vue new file mode 100644 index 00000000000..68dcfde9ae2 --- /dev/null +++ b/components/desktop/extensions/capital/features/Invest/CreateProjectInvest/ui/Fab/ProjectInvestFabAction.vue @@ -0,0 +1,39 @@ + + + diff --git a/components/desktop/extensions/capital/features/Invest/CreateProjectInvest/ui/Fab/index.ts b/components/desktop/extensions/capital/features/Invest/CreateProjectInvest/ui/Fab/index.ts new file mode 100644 index 00000000000..a11a39cadff --- /dev/null +++ b/components/desktop/extensions/capital/features/Invest/CreateProjectInvest/ui/Fab/index.ts @@ -0,0 +1,2 @@ +export { default as ProjectInvestFabAction } from './ProjectInvestFabAction.vue'; +export { default as ComponentInvestFabAction } from './ComponentInvestFabAction.vue'; diff --git a/components/desktop/extensions/capital/features/Invest/CreateProjectInvest/ui/index.ts b/components/desktop/extensions/capital/features/Invest/CreateProjectInvest/ui/index.ts index f942ea23bcd..e1d23c954b3 100644 --- a/components/desktop/extensions/capital/features/Invest/CreateProjectInvest/ui/index.ts +++ b/components/desktop/extensions/capital/features/Invest/CreateProjectInvest/ui/index.ts @@ -1 +1,3 @@ export { default as CreateProjectInvestButton } from './CreateProjectInvestButton.vue'; +export { CreateProjectInvestDialog } from './Dialog'; +export { ProjectInvestFabAction, ComponentInvestFabAction } from './Fab'; diff --git a/components/desktop/extensions/capital/features/Issue/UpdateIssue/ui/UpdatePriority/UpdatePriority.vue b/components/desktop/extensions/capital/features/Issue/UpdateIssue/ui/UpdatePriority/UpdatePriority.vue index 1443acc16ab..e731ac269d9 100644 --- a/components/desktop/extensions/capital/features/Issue/UpdateIssue/ui/UpdatePriority/UpdatePriority.vue +++ b/components/desktop/extensions/capital/features/Issue/UpdateIssue/ui/UpdatePriority/UpdatePriority.vue @@ -6,11 +6,8 @@ q-select( option-label="label" emit-value map-options - :color="getIssuePriorityColor(selectedPriority)" - text-color="white" - :bg-color="priorityBgColor" + filled dense - :standout="priorityStandout" :label="label" :readonly="readonly" @update:model-value="handlePriorityChange" @@ -21,7 +18,7 @@ q-select( import { ref, watch, computed } from 'vue' import { useRoute } from 'vue-router' import { Zeus } from '@coopenomics/sdk' -import { getIssuePriorityColor, getIssuePriorityLabel } from 'app/extensions/capital/shared/lib' +import { getIssuePriorityLabel } from 'app/extensions/capital/shared/lib' import { useUpdateIssue } from '../../model' interface Props { @@ -51,15 +48,6 @@ const { debounceSave } = useUpdateIssue() // Текущий выбранный приоритет const selectedPriority = ref(props.modelValue) -// Computed свойство для standout в зависимости от приоритета -const priorityStandout = computed(() => { - const color = getIssuePriorityColor(selectedPriority.value) - return `bg-${color} text-white` -}) - -// Computed свойство для bg-color в зависимости от приоритета -const priorityBgColor = computed(() => getIssuePriorityColor(selectedPriority.value)) - // Опции для выбора приоритета const priorityOptions = [ { value: Zeus.IssuePriority.LOW, label: getIssuePriorityLabel(Zeus.IssuePriority.LOW) }, diff --git a/components/desktop/extensions/capital/features/Project/AddAuthor/ui/AddAuthorButton.vue b/components/desktop/extensions/capital/features/Project/AddAuthor/ui/AddAuthorButton.vue index 9f7a1669219..0b4c0686262 100644 --- a/components/desktop/extensions/capital/features/Project/AddAuthor/ui/AddAuthorButton.vue +++ b/components/desktop/extensions/capital/features/Project/AddAuthor/ui/AddAuthorButton.vue @@ -1,12 +1,11 @@ diff --git a/components/desktop/extensions/capital/features/Project/SetPlan/ui/index.ts b/components/desktop/extensions/capital/features/Project/SetPlan/ui/index.ts index 7672716ef47..07b450ec5d7 100644 --- a/components/desktop/extensions/capital/features/Project/SetPlan/ui/index.ts +++ b/components/desktop/extensions/capital/features/Project/SetPlan/ui/index.ts @@ -1 +1,3 @@ export { default as SetPlanButton } from './SetPlanButton.vue'; +export { SetPlanDialog } from './Dialog'; +export { SetPlanFabAction } from './Fab'; diff --git a/components/desktop/extensions/capital/features/Story/CreateStory/ui/Dialog/CreateRequirementDialog.vue b/components/desktop/extensions/capital/features/Story/CreateStory/ui/Dialog/CreateRequirementDialog.vue new file mode 100644 index 00000000000..6f93fc520a9 --- /dev/null +++ b/components/desktop/extensions/capital/features/Story/CreateStory/ui/Dialog/CreateRequirementDialog.vue @@ -0,0 +1,169 @@ + + + diff --git a/components/desktop/extensions/capital/features/Story/CreateStory/ui/Dialog/index.ts b/components/desktop/extensions/capital/features/Story/CreateStory/ui/Dialog/index.ts new file mode 100644 index 00000000000..bdfceae9855 --- /dev/null +++ b/components/desktop/extensions/capital/features/Story/CreateStory/ui/Dialog/index.ts @@ -0,0 +1 @@ +export { default as CreateRequirementDialog } from './CreateRequirementDialog.vue'; diff --git a/components/desktop/extensions/capital/features/Story/CreateStory/ui/Fab/CreateRequirementFabAction.vue b/components/desktop/extensions/capital/features/Story/CreateStory/ui/Fab/CreateRequirementFabAction.vue new file mode 100644 index 00000000000..5e2cfa794ad --- /dev/null +++ b/components/desktop/extensions/capital/features/Story/CreateStory/ui/Fab/CreateRequirementFabAction.vue @@ -0,0 +1,34 @@ + + + diff --git a/components/desktop/extensions/capital/features/Story/CreateStory/ui/Fab/index.ts b/components/desktop/extensions/capital/features/Story/CreateStory/ui/Fab/index.ts new file mode 100644 index 00000000000..869c0e115de --- /dev/null +++ b/components/desktop/extensions/capital/features/Story/CreateStory/ui/Fab/index.ts @@ -0,0 +1 @@ +export { default as CreateRequirementFabAction } from './CreateRequirementFabAction.vue'; diff --git a/components/desktop/extensions/capital/features/Story/CreateStory/ui/index.ts b/components/desktop/extensions/capital/features/Story/CreateStory/ui/index.ts index f60d82d3fd5..2a6c3f9d625 100644 --- a/components/desktop/extensions/capital/features/Story/CreateStory/ui/index.ts +++ b/components/desktop/extensions/capital/features/Story/CreateStory/ui/index.ts @@ -1 +1,3 @@ export { default as CreateStoryButton } from './CreateStoryButton.vue'; +export { CreateRequirementFabAction } from './Fab'; +export { CreateRequirementDialog } from './Dialog'; diff --git a/components/desktop/extensions/capital/features/Story/DeleteStory/ui/DeleteStoryButton.vue b/components/desktop/extensions/capital/features/Story/DeleteStory/ui/DeleteStoryButton.vue index 4a83bbd21e7..d2637dc528b 100644 --- a/components/desktop/extensions/capital/features/Story/DeleteStory/ui/DeleteStoryButton.vue +++ b/components/desktop/extensions/capital/features/Story/DeleteStory/ui/DeleteStoryButton.vue @@ -9,7 +9,6 @@ q-btn( :loading='isSubmitting' ) q-icon(name='delete') - q-dialog(v-model='showDialog', @hide='close') ModalBase(title='Удаление истории') Form.q-pa-sm( @@ -21,7 +20,6 @@ q-btn( ) div(style='max-width: 300px') p Вы уверены, что хотите удалить историю? - .text-body2.text-grey-6.q-mt-sm {{ storyTitle }} - - diff --git a/components/desktop/extensions/capital/pages/ComponentAuthorsPage/ui/index.ts b/components/desktop/extensions/capital/pages/ComponentAuthorsPage/ui/index.ts deleted file mode 100644 index 9b5ffd972cc..00000000000 --- a/components/desktop/extensions/capital/pages/ComponentAuthorsPage/ui/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from './ComponentAuthorsPage.vue'; diff --git a/components/desktop/extensions/capital/pages/ComponentContributorsPage/ui/ComponentContributorsPage.vue b/components/desktop/extensions/capital/pages/ComponentContributorsPage/ui/ComponentContributorsPage.vue index 30191bdd0a0..80e71afbbac 100644 --- a/components/desktop/extensions/capital/pages/ComponentContributorsPage/ui/ComponentContributorsPage.vue +++ b/components/desktop/extensions/capital/pages/ComponentContributorsPage/ui/ComponentContributorsPage.vue @@ -1,8 +1,5 @@ diff --git a/components/desktop/extensions/capital/pages/ComponentPage/ui/ComponentPage.vue b/components/desktop/extensions/capital/pages/ComponentPage/ui/ComponentPage.vue index bf3ac1a6299..0f101687a42 100644 --- a/components/desktop/extensions/capital/pages/ComponentPage/ui/ComponentPage.vue +++ b/components/desktop/extensions/capital/pages/ComponentPage/ui/ComponentPage.vue @@ -6,11 +6,28 @@ div // Floating Action Button Fab(v-if="project") template(#actions v-if="project?.permissions?.has_clearance") - // Показываем кнопку создания задачи, если пользователь имеет допуск к проекту + // Показываем кнопку создания задачи и требования, если пользователь имеет допуск к проекту CreateIssueFabAction( :project-hash="projectHash" @action-completed="handleIssueCreated" ) + CreateRequirementFabAction( + :filter="{ project_hash: projectHash }" + @action-completed="handleRequirementCreated" + ) + SetPlanFabAction( + v-if="project?.permissions?.can_set_plan" + :project="project" + @action-completed="handlePlanSet" + ) + AddAuthorFabAction( + :project="project" + @action-completed="handleAuthorsAdded" + ) + ComponentInvestFabAction( + :project="project" + @action-completed="handleInvestCompleted" + ) template template(#default v-if="project?.permissions?.pending_clearance") @@ -33,15 +50,21 @@ div diff --git a/components/desktop/extensions/capital/pages/ComponentTasksPage/ui/ComponentTasksPage.vue b/components/desktop/extensions/capital/pages/ComponentTasksPage/ui/ComponentTasksPage.vue index b2391d748da..9538aca5f01 100644 --- a/components/desktop/extensions/capital/pages/ComponentTasksPage/ui/ComponentTasksPage.vue +++ b/components/desktop/extensions/capital/pages/ComponentTasksPage/ui/ComponentTasksPage.vue @@ -54,6 +54,9 @@ const handleIssueClick = (issue: IIssue) => { project_hash: projectHash.value, issue_hash: issue.issue_hash, }, + query: { + _backRoute: 'component-tasks' + } }); }; diff --git a/components/desktop/extensions/capital/pages/IssuePage/ui/IssuePage.vue b/components/desktop/extensions/capital/pages/IssuePage/ui/IssuePage.vue index 6e7530555a8..346cbdff093 100644 --- a/components/desktop/extensions/capital/pages/IssuePage/ui/IssuePage.vue +++ b/components/desktop/extensions/capital/pages/IssuePage/ui/IssuePage.vue @@ -1,22 +1,16 @@ @@ -59,7 +55,7 @@ import { StoriesWidget } from 'app/extensions/capital/widgets/StoryWidget'; import { Editor, AutoSaveIndicator } from 'src/shared/ui'; import { textToEditorJS } from 'src/shared/lib/utils/editorjs'; import { useUpdateIssue } from 'app/extensions/capital/features/Issue/UpdateIssue'; -import { IssueControls } from 'app/extensions/capital/widgets'; +import { IssueControls, IssueTitleEditor } from 'app/extensions/capital/widgets'; const route = useRoute(); const issue = ref(null); @@ -98,17 +94,16 @@ const ensureEditorJSFormat = (description: any) => { return textToEditorJS(String(description)); }; -// Обработчик изменения названия задачи -const handleTitleChange = () => { - if (!issue.value) return; +// Обработчик изменения полей +const handleFieldChange = () => { + // Просто триггер реактивности для computed hasChanges в виджетах +}; - const updateData = { - issue_hash: issue.value.issue_hash, - title: issue.value.title, - }; - - // Запускаем авто-сохранение с задержкой - debounceSave(updateData, projectHash.value); +// Обработчик обновления названия задачи +const handleTitleUpdate = (value: string) => { + if (issue.value) { + issue.value.title = value; + } }; // Обработчик изменения описания задачи @@ -129,8 +124,10 @@ const handleDescriptionChange = () => { const { registerAction: registerRightDrawerAction } = useRightDrawer(); // Настраиваем кнопку "Назад" +// По умолчанию используем router.back(), но можно переопределить через query параметр _backRoute useBackButton({ text: 'Назад', + routeName: route.query._backRoute as string || undefined, componentId: 'issue-page-' + issueHash.value, }); diff --git a/components/desktop/extensions/capital/pages/ProjectAuthorsPage/index.ts b/components/desktop/extensions/capital/pages/ProjectAuthorsPage/index.ts deleted file mode 100644 index ae8b253ba3a..00000000000 --- a/components/desktop/extensions/capital/pages/ProjectAuthorsPage/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default as ProjectAuthorsPage } from './ui/ProjectAuthorsPage.vue'; diff --git a/components/desktop/extensions/capital/pages/ProjectAuthorsPage/ui/ProjectAuthorsPage.vue b/components/desktop/extensions/capital/pages/ProjectAuthorsPage/ui/ProjectAuthorsPage.vue deleted file mode 100644 index 1a2b07c6adc..00000000000 --- a/components/desktop/extensions/capital/pages/ProjectAuthorsPage/ui/ProjectAuthorsPage.vue +++ /dev/null @@ -1,25 +0,0 @@ - - - - - diff --git a/components/desktop/extensions/capital/pages/ProjectContributorsPage/ui/ProjectContributorsPage.vue b/components/desktop/extensions/capital/pages/ProjectContributorsPage/ui/ProjectContributorsPage.vue index 82fbed2740c..1921a8cdf9a 100644 --- a/components/desktop/extensions/capital/pages/ProjectContributorsPage/ui/ProjectContributorsPage.vue +++ b/components/desktop/extensions/capital/pages/ProjectContributorsPage/ui/ProjectContributorsPage.vue @@ -1,8 +1,5 @@ diff --git a/components/desktop/extensions/capital/pages/ProjectPage/ui/ProjectPage.vue b/components/desktop/extensions/capital/pages/ProjectPage/ui/ProjectPage.vue index f4b4986db0f..1da1052970f 100644 --- a/components/desktop/extensions/capital/pages/ProjectPage/ui/ProjectPage.vue +++ b/components/desktop/extensions/capital/pages/ProjectPage/ui/ProjectPage.vue @@ -3,25 +3,48 @@ div // Контент страницы проекта router-view - // Floating Action Button для создания компонента + // Floating Action Button для создания компонента, требования и установки плана Fab(v-if="project") template(#actions) CreateComponentFabAction( :project="project" @action-completed="handleComponentCreated" ) + CreateRequirementFabAction( + :filter="{ project_hash: projectHash }" + @action-completed="handleRequirementCreated" + ) + SetPlanFabAction( + v-if="project?.permissions?.can_set_plan" + :project="project" + @action-completed="handlePlanSet" + ) + AddAuthorFabAction( + :project="project" + @action-completed="handleAuthorsAdded" + ) + ProjectInvestFabAction( + :project="project" + @action-completed="handleInvestCompleted" + ) diff --git a/components/desktop/extensions/capital/pages/ProjectPlanningPage/ui/ProjectPlanningPage.vue b/components/desktop/extensions/capital/pages/ProjectPlanningPage/ui/ProjectPlanningPage.vue index 46368ed3100..0ff61db8eaa 100644 --- a/components/desktop/extensions/capital/pages/ProjectPlanningPage/ui/ProjectPlanningPage.vue +++ b/components/desktop/extensions/capital/pages/ProjectPlanningPage/ui/ProjectPlanningPage.vue @@ -1,21 +1,14 @@ diff --git a/components/desktop/extensions/capital/pages/ProjectsListPage/ui/ProjectsListPage.vue b/components/desktop/extensions/capital/pages/ProjectsListPage/ui/ProjectsListPage.vue index 1d354239165..0b55596c98c 100644 --- a/components/desktop/extensions/capital/pages/ProjectsListPage/ui/ProjectsListPage.vue +++ b/components/desktop/extensions/capital/pages/ProjectsListPage/ui/ProjectsListPage.vue @@ -16,13 +16,14 @@ div :has-issues-with-creators='hasIssuesWithCreators', :master='master', @toggle-expand='handleProjectToggleExpand', - @data-loaded='handleProjectsDataLoaded' + @data-loaded='handleProjectsDataLoaded', + @open-project='handleOpenProject' ) template(#project-content='{ project }') ComponentsListWidget( :components='project.components', :expanded='expandedComponents', - @open-component='(componentHash) => router.push({ name: "component-description", params: { project_hash: componentHash } })', + @open-component='(componentHash) => router.push({ name: "component-description", params: { project_hash: componentHash }, query: { _backRoute: "projects-list" } })', @toggle-component='handleComponentToggle' ) template(#component-content='{ component }') @@ -32,7 +33,7 @@ div :priorities='componentPriorities', :creators='componentCreators', :master='componentMaster', - @issue-click='(issue) => router.push({ name: "component-issue", params: { project_hash: issue.project_hash, issue_hash: issue.issue_hash } })' + @issue-click='(issue) => router.push({ name: "component-issue", params: { project_hash: issue.project_hash, issue_hash: issue.issue_hash }, query: { _backRoute: "projects-list" } })' ) // Floating Action Button для создания проекта @@ -101,6 +102,10 @@ const handleComponentToggle = (componentHash: string) => { toggleComponentExpanded(componentHash); }; +const handleOpenProject = (projectHash: string) => { + router.push({ name: 'project-description', params: { project_hash: projectHash }, query: { _backRoute: 'projects-list' } }); +}; + const handleProjectsDataLoaded = (projectHashes: string[], totalComponents?: number) => { // Очищаем устаревшие записи expanded проектов после загрузки данных cleanupProjectsExpanded(projectHashes); diff --git a/components/desktop/extensions/capital/pages/index.ts b/components/desktop/extensions/capital/pages/index.ts index 4a2803bf4e5..cf05d84dced 100644 --- a/components/desktop/extensions/capital/pages/index.ts +++ b/components/desktop/extensions/capital/pages/index.ts @@ -16,7 +16,6 @@ export * from './MasterCommitsPage'; export * from './ProjectDescriptionPage'; export * from './ProjectInviteEditorPage'; export * from './ProjectPlanningPage'; -export * from './ProjectAuthorsPage'; export * from './ProjectContributorsPage'; export * from './ProjectComponentsPage'; export * from './ProjectRequirementsPage'; diff --git a/components/desktop/extensions/capital/shared/composables/useListNavigation.ts b/components/desktop/extensions/capital/shared/composables/useListNavigation.ts new file mode 100644 index 00000000000..561622f0901 --- /dev/null +++ b/components/desktop/extensions/capital/shared/composables/useListNavigation.ts @@ -0,0 +1,24 @@ +import { useRouter } from 'vue-router'; + +export function useListNavigation() { + const router = useRouter(); + + const navigateToProject = (projectHash: string) => { + router.push({ + name: 'project-description', + params: { project_hash: projectHash } + }); + }; + + const navigateToComponent = (componentHash: string) => { + router.push({ + name: 'component-description', + params: { project_hash: componentHash } + }); + }; + + return { + navigateToProject, + navigateToComponent + }; +} diff --git a/components/desktop/extensions/capital/widgets/ComponentsListWidget/ComponentsListWidget.vue b/components/desktop/extensions/capital/widgets/ComponentsListWidget/ComponentsListWidget.vue index 9fcb9d3049d..93f869df18e 100644 --- a/components/desktop/extensions/capital/widgets/ComponentsListWidget/ComponentsListWidget.vue +++ b/components/desktop/extensions/capital/widgets/ComponentsListWidget/ComponentsListWidget.vue @@ -31,8 +31,11 @@ q-card(flat) // ID с иконкой (100px + отступ 30px) .col-auto(style='width: 100px; padding-left: 30px; flex-shrink: 0') - q-icon(name='extension', size='xs', color='primary').q-mr-xs - span(v-if='props.row.prefix').text-grey-7 {{ '#' + props.row.prefix }} + q-icon(name='extension', size='xs').q-mr-xs + span.list-item-title( + v-if='props.row.prefix' + @click.stop='handleOpenComponent(props.row.project_hash)' + ) {{ '#' + props.row.prefix }} // Title со статусом (400px + отступ 30px) .col(style='width: 400px; padding-left: 30px') @@ -47,14 +50,20 @@ q-card(flat) ).q-mr-sm span {{ props.row.title }} - // Actions - CreateIssueButton (120px, выравнивание по правому краю) - .col-auto.ml-auto(style='width: 120px') - .row.items-center.justify-end + // Actions - CreateIssueButton и кнопка перехода (140px, выравнивание по правому краю) + .col-auto.ml-auto(style='width: 140px') + .row.items-center.justify-end.q-gutter-xs CreateIssueButton( @click.stop, :mini='true', :project-hash='props.row.project_hash' ) + q-btn( + size='xs', + flat, + icon='arrow_forward', + @click.stop='handleOpenComponent(props.row.project_hash)' + ) //- ProjectMenuWidget(:project='props.row', @click.stop) // Слот для дополнительного контента компонента @@ -208,4 +217,15 @@ const columns = [ .q-chip { font-weight: 500; } + +// Импорт глобального стиля для подсветки +:deep(.list-item-title) { + font-weight: 500; + cursor: pointer; + transition: color 0.2s ease; + + &:hover { + color: var(--q-accent); + } +} diff --git a/components/desktop/extensions/capital/widgets/IssueTitleEditor/IssueTitleEditor.vue b/components/desktop/extensions/capital/widgets/IssueTitleEditor/IssueTitleEditor.vue new file mode 100644 index 00000000000..a3736f1007f --- /dev/null +++ b/components/desktop/extensions/capital/widgets/IssueTitleEditor/IssueTitleEditor.vue @@ -0,0 +1,76 @@ + + + + + diff --git a/components/desktop/extensions/capital/widgets/IssueTitleEditor/index.ts b/components/desktop/extensions/capital/widgets/IssueTitleEditor/index.ts new file mode 100644 index 00000000000..c06d524a9fa --- /dev/null +++ b/components/desktop/extensions/capital/widgets/IssueTitleEditor/index.ts @@ -0,0 +1 @@ +export { default as IssueTitleEditor } from './IssueTitleEditor.vue' diff --git a/components/desktop/extensions/capital/widgets/IssuesListWidget/ui/IssuesListWidget.vue b/components/desktop/extensions/capital/widgets/IssuesListWidget/ui/IssuesListWidget.vue index 8aac330c7ec..4da422f86cf 100644 --- a/components/desktop/extensions/capital/widgets/IssuesListWidget/ui/IssuesListWidget.vue +++ b/components/desktop/extensions/capital/widgets/IssuesListWidget/ui/IssuesListWidget.vue @@ -25,8 +25,10 @@ div // ID с иконкой (100px + отступ 40px) .col-auto(style='width: 100px; padding-left: 40px; flex-shrink: 0') - q-icon(name='task', size='xs', color='primary').q-mr-xs - span.text-grey-7 {{ '#' + props.row.id }} + q-icon(name='task', size='xs').q-mr-xs + span.list-item-title( + @click.stop='handleIssueClick(props.row)' + ) {{ '#' + props.row.id }} // Title с приоритетом (400px + отступ 40px) .col(style='width: 400px; padding-left: 40px') @@ -41,9 +43,9 @@ div ).q-mr-sm span.text-body2.font-weight-medium {{ props.row.title }} - // Actions (статус + исполнитель) - выравнивание по правому краю + // Actions (статус + исполнитель + кнопка перехода) - выравнивание по правому краю .col-auto.ml-auto - .row.items-center.justify-end.q-gutter-sm + .row.items-center.justify-end.q-gutter-xs UpdateStatus( :model-value='props.row.status' :issue-hash='props.row.issue_hash' @@ -58,10 +60,16 @@ div @click.stop style="max-width: 250px;" ) + + q-btn( + size='xs', + flat, + icon='arrow_forward', + @click.stop='handleIssueClick(props.row)' + ) - - diff --git a/components/desktop/extensions/capital/widgets/ProjectInfoSelectorWidget/ProjectContributorsList.vue b/components/desktop/extensions/capital/widgets/ProjectInfoSelectorWidget/ProjectContributorsList.vue index ac35a586161..7390f5c9897 100644 --- a/components/desktop/extensions/capital/widgets/ProjectInfoSelectorWidget/ProjectContributorsList.vue +++ b/components/desktop/extensions/capital/widgets/ProjectInfoSelectorWidget/ProjectContributorsList.vue @@ -1,27 +1,95 @@ diff --git a/components/desktop/extensions/capital/widgets/ProjectInfoSelectorWidget/ProjectInfoSelectorWidget.vue b/components/desktop/extensions/capital/widgets/ProjectInfoSelectorWidget/ProjectInfoSelectorWidget.vue index 89d7a3b12d2..1b0c8c33fe1 100644 --- a/components/desktop/extensions/capital/widgets/ProjectInfoSelectorWidget/ProjectInfoSelectorWidget.vue +++ b/components/desktop/extensions/capital/widgets/ProjectInfoSelectorWidget/ProjectInfoSelectorWidget.vue @@ -25,13 +25,6 @@ div label="Финансирование", @click="activeTab = 'planning'" ) - q-btn( - size="md" - :outline="activeTab !== 'authors'" - color="primary" - label="Соавторы" - @click="activeTab = 'authors'" - ) q-btn( size="md" :outline="activeTab !== 'contributors'" @@ -60,15 +53,8 @@ div :readonly="!permissions?.can_edit_project" ) - // Планирование проекта - div(v-if="activeTab === 'planning' && permissions?.can_edit_project") - ProjectPlanning(:project='project') - // Соавторы проекта - div(v-if="activeTab === 'authors'" ) - ProjectAuthorsList(:project='project' style="min-height: 300px;") - // Участники проекта div(v-if="activeTab === 'contributors'" ) ProjectContributorsList(:project='project' style="min-height: 300px;") @@ -79,8 +65,7 @@ div import { ref, computed, watch } from 'vue'; import type { IProject, IProjectPermissions } from 'app/extensions/capital/entities/Project/model'; import { Editor } from 'src/shared/ui'; -import ProjectPlanning from './ProjectPlanning.vue'; -import ProjectAuthorsList from './ProjectAuthorsList.vue'; + import ProjectContributorsList from './ProjectContributorsList.vue'; const props = defineProps<{ @@ -97,7 +82,7 @@ const emit = defineEmits<{ }>(); // Активная вкладка -const activeTab = ref<'description' | 'invite' | 'management' | 'planning' | 'authors' | 'contributors'>('description'); +const activeTab = ref<'description' | 'invite' | 'management' | 'planning' | 'contributors'>('description'); // Следим за изменением permissions и переключаем на доступную вкладку watch(() => props.permissions, (newPermissions) => { diff --git a/components/desktop/extensions/capital/widgets/ProjectInfoSelectorWidget/ProjectPlanning.vue b/components/desktop/extensions/capital/widgets/ProjectInfoSelectorWidget/ProjectPlanning.vue deleted file mode 100644 index 271757ef462..00000000000 --- a/components/desktop/extensions/capital/widgets/ProjectInfoSelectorWidget/ProjectPlanning.vue +++ /dev/null @@ -1,243 +0,0 @@ - - - diff --git a/components/desktop/extensions/capital/widgets/ProjectPlanningWidget/ProjectPlanningWidget.vue b/components/desktop/extensions/capital/widgets/ProjectPlanningWidget/ProjectPlanningWidget.vue new file mode 100644 index 00000000000..97656523376 --- /dev/null +++ b/components/desktop/extensions/capital/widgets/ProjectPlanningWidget/ProjectPlanningWidget.vue @@ -0,0 +1,98 @@ + + + diff --git a/components/desktop/extensions/capital/widgets/ProjectPlanningWidget/index.ts b/components/desktop/extensions/capital/widgets/ProjectPlanningWidget/index.ts new file mode 100644 index 00000000000..4863c5fe43d --- /dev/null +++ b/components/desktop/extensions/capital/widgets/ProjectPlanningWidget/index.ts @@ -0,0 +1 @@ +export { default as ProjectPlanningWidget } from './ProjectPlanningWidget.vue'; diff --git a/components/desktop/extensions/capital/widgets/ProjectsListWidget/ProjectsListWidget.vue b/components/desktop/extensions/capital/widgets/ProjectsListWidget/ProjectsListWidget.vue index 2816d0fda4d..3c89c804717 100644 --- a/components/desktop/extensions/capital/widgets/ProjectsListWidget/ProjectsListWidget.vue +++ b/components/desktop/extensions/capital/widgets/ProjectsListWidget/ProjectsListWidget.vue @@ -34,13 +34,16 @@ q-card(flat) // ID с иконкой (100px + отступ 0px) .col-auto(style='width: 100px; padding-left: 0px; flex-shrink: 0') - q-icon(name='work', size='xs', color='primary').q-mr-xs - span.text-grey-7(v-if='props.row.prefix') {{ '#' + props.row.prefix }} + q-icon(name='work', size='xs').q-mr-xs + span.list-item-title( + v-if='props.row.prefix' + @click.stop='handleOpenProject(props.row.project_hash)' + ) {{ '#' + props.row.prefix }} // Title со статусом (400px + отступ 0px) .col(style='width: 400px; padding-left: 0px') .list-item-title( - @click.stop='() => router.push({ name: "project-description", params: { project_hash: props.row.project_hash } })' + @click.stop='handleOpenProject(props.row.project_hash)' style='display: inline-block; vertical-align: top; word-wrap: break-word; white-space: normal' ) q-icon( @@ -50,14 +53,22 @@ q-card(flat) ).q-mr-sm span {{ props.row.title }} - // Actions - CreateComponentButton (100px, выравнивание по правому краю) - .col-auto.ml-auto(style='width: 100px') - .row.items-center.justify-end + // Actions - CreateComponentButton и кнопка перехода (120px, выравнивание по правому краю) + .col-auto.ml-auto(style='width: 120px') + .row.items-center.justify-end.q-gutter-xs CreateComponentButton( :project='props.row', :mini='true', @click.stop ) + + q-btn( + size='xs', + flat, + icon='arrow_forward', + @click.stop='handleOpenProject(props.row.project_hash)' + ) + // Слот для дополнительного контента проекта (ComponentsListWidget) q-tr.q-virtual-scroll--with-prev( no-hover, @@ -70,7 +81,6 @@ q-card(flat) + + diff --git a/components/desktop/extensions/capital/widgets/StoryWidget/ui/StoriesWidget.vue b/components/desktop/extensions/capital/widgets/StoryWidget/ui/StoriesWidget.vue index e609f028c29..105c120f303 100644 --- a/components/desktop/extensions/capital/widgets/StoryWidget/ui/StoriesWidget.vue +++ b/components/desktop/extensions/capital/widgets/StoryWidget/ui/StoriesWidget.vue @@ -12,7 +12,7 @@ div v-if='canCreate', ref='titleInput', v-model='newStoryTitle', - placeholder='Введите историю...', + placeholder='Введите требование...', dense, flat, hide-bottom-space, @@ -81,7 +81,7 @@ div div(v-if='loading') .text-center.q-pa-md q-spinner(color='primary', size='24px') - .text-body2.text-grey-6.q-mt-sm Загрузка историй... + .text-body2.text-grey-6.q-mt-sm Загрузка требований... + + diff --git a/components/desktop/extensions/chairman/features/UpdateSettings/ui/index.ts b/components/desktop/extensions/chairman/features/UpdateSettings/ui/index.ts new file mode 100644 index 00000000000..83d54e2ca2d --- /dev/null +++ b/components/desktop/extensions/chairman/features/UpdateSettings/ui/index.ts @@ -0,0 +1 @@ +export { default as UpdateSettingsForm } from './UpdateSettingsForm.vue' diff --git a/components/desktop/extensions/chairman/features/index.ts b/components/desktop/extensions/chairman/features/index.ts index eefdb03e7a0..f84b4bdb36e 100644 --- a/components/desktop/extensions/chairman/features/index.ts +++ b/components/desktop/extensions/chairman/features/index.ts @@ -1 +1,2 @@ export * as Approval from './Approval'; +export * as UpdateSettings from './UpdateSettings'; diff --git a/components/desktop/extensions/chairman/install.ts b/components/desktop/extensions/chairman/install.ts index de3056f04b8..4bef494d99d 100644 --- a/components/desktop/extensions/chairman/install.ts +++ b/components/desktop/extensions/chairman/install.ts @@ -9,6 +9,7 @@ import { ChangeCooperativeContacts } from 'src/pages/Cooperative/ChangeContacts' import { MembersPage } from 'src/pages/Cooperative/MembersPage'; import { CooperativeKeyPage } from 'src/pages/Cooperative/CooperativeKey'; import { ApprovalsPage } from 'app/extensions/chairman/pages/ApprovalsPage'; +import { SystemSettingsPage } from 'app/extensions/chairman/pages/SystemSettingsPage'; import { agreementsBase } from 'src/shared/lib/consts/workspaces'; import type { IWorkspaceConfig } from 'src/shared/lib/types/workspace'; @@ -114,6 +115,18 @@ export default async function (): Promise { }, ], }, + { + path: 'system-settings', + name: 'system-settings', + component: markRaw(SystemSettingsPage), + meta: { + title: 'Настройки системы', + icon: 'fa-solid fa-cogs', + roles: ['chairman'], + agreements: agreementsBase, + requiresAuth: true, + }, + }, { path: 'settings/members', name: 'members', diff --git a/components/desktop/extensions/chairman/pages/SystemSettingsPage/index.ts b/components/desktop/extensions/chairman/pages/SystemSettingsPage/index.ts new file mode 100644 index 00000000000..ed584959d76 --- /dev/null +++ b/components/desktop/extensions/chairman/pages/SystemSettingsPage/index.ts @@ -0,0 +1 @@ +export * from './ui' diff --git a/components/desktop/extensions/chairman/pages/SystemSettingsPage/ui/SystemSettingsPage.vue b/components/desktop/extensions/chairman/pages/SystemSettingsPage/ui/SystemSettingsPage.vue new file mode 100644 index 00000000000..be680695544 --- /dev/null +++ b/components/desktop/extensions/chairman/pages/SystemSettingsPage/ui/SystemSettingsPage.vue @@ -0,0 +1,86 @@ + + + + + diff --git a/components/desktop/extensions/chairman/pages/SystemSettingsPage/ui/index.ts b/components/desktop/extensions/chairman/pages/SystemSettingsPage/ui/index.ts new file mode 100644 index 00000000000..fb99cba490c --- /dev/null +++ b/components/desktop/extensions/chairman/pages/SystemSettingsPage/ui/index.ts @@ -0,0 +1 @@ +export { default as SystemSettingsPage } from './SystemSettingsPage.vue' diff --git a/components/desktop/extensions/chairman/pages/index.ts b/components/desktop/extensions/chairman/pages/index.ts index e46aef2398f..f67a7613940 100644 --- a/components/desktop/extensions/chairman/pages/index.ts +++ b/components/desktop/extensions/chairman/pages/index.ts @@ -1 +1,2 @@ export * as ApprovalsPage from './ApprovalsPage'; +export * as SystemSettingsPage from './SystemSettingsPage'; diff --git a/components/desktop/src/entities/Desktop/model/store.ts b/components/desktop/src/entities/Desktop/model/store.ts index 13c02bb2275..8abe92709e1 100644 --- a/components/desktop/src/entities/Desktop/model/store.ts +++ b/components/desktop/src/entities/Desktop/model/store.ts @@ -7,8 +7,8 @@ import type { IDesktopWithNavigation, } from './types'; import { api } from '../api'; -import { useCurrentUser } from 'src/entities/Session'; import { useSystemStore } from 'src/entities/System/model'; +import { useCurrentUser } from 'src/entities/Session'; interface WorkspaceMenuItem { workspaceName: string; @@ -117,30 +117,37 @@ export const useDesktopStore = defineStore(namespace, () => { savedWorkspace && currentDesktop.value?.workspaces.some((ws) => ws.name === savedWorkspace) ) { - // Устанавливаем рабочий стол без включения состояния загрузки (это инициализация) + // Устанавливаем сохраненный рабочий стол без включения состояния загрузки (это инициализация) activeWorkspaceName.value = savedWorkspace; - localStorage.setItem(STORAGE_KEY_WORKSPACE, savedWorkspace); return; } - // Если нет сохраненного рабочего стола, определяем по правам пользователя - const userStore = useCurrentUser(); + // Получаем настройки системы + const systemStore = useSystemStore(); + const currentUser = useCurrentUser(); - if (userStore.isMember || userStore.isChairman) { - // Для членов совета или председателя устанавливаем soviet - const hasSoviet = currentDesktop.value?.workspaces.some( - (ws) => ws.name === 'soviet', - ); - if (hasSoviet) { - activeWorkspaceName.value = 'soviet'; - localStorage.setItem(STORAGE_KEY_WORKSPACE, 'soviet'); - return; - } + let defaultWorkspace = 'participant'; // дефолтное значение + + // Определяем, какие настройки использовать (авторизованный или неавторизованный пользователь) + if (currentUser.isAuth) { + // Для авторизованных пользователей используем authorized_default_workspace + defaultWorkspace = systemStore.info?.settings?.authorized_default_workspace || 'participant'; + } else { + // Для неавторизованных пользователей используем non_authorized_default_workspace + defaultWorkspace = systemStore.info?.settings?.non_authorized_default_workspace || 'participant'; } - // В остальных случаях устанавливаем participant + // Проверяем, что выбранный рабочий стол доступен + const isWorkspaceAvailable = currentDesktop.value?.workspaces.some((ws) => ws.name === defaultWorkspace); + + if (isWorkspaceAvailable) { + activeWorkspaceName.value = defaultWorkspace; + localStorage.setItem(STORAGE_KEY_WORKSPACE, defaultWorkspace); + } else { + // Если настроенный рабочий стол недоступен, используем participant activeWorkspaceName.value = 'participant'; localStorage.setItem(STORAGE_KEY_WORKSPACE, 'participant'); + } } const activeSecondLevelRoutes = computed((): RouteRecordRaw[] => { @@ -220,6 +227,7 @@ export const useDesktopStore = defineStore(namespace, () => { params: Record; } | null { const { info } = useSystemStore(); + const currentUser = useCurrentUser(); if (!currentDesktop.value || !activeWorkspaceName.value) { return null; @@ -234,7 +242,37 @@ export const useDesktopStore = defineStore(namespace, () => { return null; } - // Проверяем наличие defaultRoute + // Проверяем, есть ли настроенный маршрут для текущего рабочего стола + let configuredRoute: string | undefined; + + if (currentUser.isAuth) { + // Для авторизованных пользователей используем authorized_default_route + configuredRoute = info?.settings?.authorized_default_route; + } else { + // Для неавторизованных пользователей используем non_authorized_default_route + configuredRoute = info?.settings?.non_authorized_default_route; + } + + // Если настроенный маршрут существует и соответствует текущему рабочему столу, используем его + if (configuredRoute) { + // Проверяем, что маршрут принадлежит текущему рабочему столу + const ws = workspaceMenus.value.find( + (menu) => menu.workspaceName === activeWorkspaceName.value, + ); + if ( + ws && + ws.mainRoute && + ws.mainRoute.children && + ws.mainRoute.children.some((child: any) => child.name === configuredRoute) + ) { + return { + name: configuredRoute, + params: { coopname: info.coopname }, + }; + } + } + + // Если нет настроенного маршрута или он не найден, используем defaultRoute рабочего стола if ((currentWorkspace as any).defaultRoute) { return { name: (currentWorkspace as any).defaultRoute, diff --git a/components/desktop/src/pages/ExtensionStore/ExtensionsManagement/ExtensionsManagement.vue b/components/desktop/src/pages/ExtensionStore/ExtensionsManagement/ExtensionsManagement.vue index a81e4ed99d5..f9a78614890 100644 --- a/components/desktop/src/pages/ExtensionStore/ExtensionsManagement/ExtensionsManagement.vue +++ b/components/desktop/src/pages/ExtensionStore/ExtensionsManagement/ExtensionsManagement.vue @@ -16,7 +16,7 @@ const menuButtons = computed(() => [ component: markRaw(RouteMenuButton), props: { routeName: 'extstore-showcase', - label: 'Витрина расширений', + label: 'Витрина', }, order: 1, }, diff --git a/components/desktop/src/pages/PermissionDenied/PermissionDenied.vue b/components/desktop/src/pages/PermissionDenied/PermissionDenied.vue index 5524451cf84..f011dc086a4 100644 --- a/components/desktop/src/pages/PermissionDenied/PermissionDenied.vue +++ b/components/desktop/src/pages/PermissionDenied/PermissionDenied.vue @@ -42,13 +42,7 @@ function goBack() { } } - // Если ничего не найдено, идем на главную страницу неавторизованного пользователя - const homePage = desktops.currentDesktop?.nonAuthorizedHome; - if (homePage) { - router.push({ name: homePage }); - } else { - // В крайнем случае, просто перезагружаем страницу - window.location.reload(); - } + // Если ничего не найдено, идем на страницу по умолчанию для неавторизованного пользователя + desktops.goToDefaultPage(router); } diff --git a/components/desktop/src/processes/navigation-guard-setup/index.ts b/components/desktop/src/processes/navigation-guard-setup/index.ts index 8696e85ebbe..b1ab52092cf 100644 --- a/components/desktop/src/processes/navigation-guard-setup/index.ts +++ b/components/desktop/src/processes/navigation-guard-setup/index.ts @@ -82,9 +82,14 @@ export function setupNavigationGuard(router: Router) { } return; } else { - // Если пользователь не авторизован, используем nonAuthorizedHome - const homePage = desktops.currentDesktop?.nonAuthorizedHome; - next({ name: homePage, params: { coopname: info.coopname } }); + // Если пользователь не авторизован, выбираем рабочий стол и переходим на страницу по умолчанию + desktops.selectDefaultWorkspace(); + const defaultRoute = desktops.getDefaultPageRoute(); + if (defaultRoute) { + next(defaultRoute); + } else { + next({ name: 'signup', params: { coopname: info.coopname } }); + } return; } } diff --git a/components/desktop/src/shared/ui/Editor/Editor.vue b/components/desktop/src/shared/ui/Editor/Editor.vue index cb0ef0cfcfd..be90f2bca52 100644 --- a/components/desktop/src/shared/ui/Editor/Editor.vue +++ b/components/desktop/src/shared/ui/Editor/Editor.vue @@ -1,5 +1,5 @@