document names refactoring & context component

This commit is contained in:
Alex Ant
2026-01-22 18:13:45 +05:00
parent c11e922573
commit b807df3ee8
132 changed files with 553 additions and 1948 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
{
// Enable the ESlint flat config support
"prettier.enable": false,
"editor.formatOnSave": true,
"editor.formatOnSave": false,
// Auto fix
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit"
@@ -3,7 +3,7 @@ import { signAgreement } from '../soviet/signAgreement'
import { getCoopProgramWallet, getUserProgramWallet } from '../wallet/walletUtils'
import { sourceProgramId, sourceProgramName } from './consts'
export async function signGenerationAgreement(
export async function signGenerationContract(
blockchain: any,
coopname: string,
username: string,
@@ -51,8 +51,8 @@ import { client } from 'src/shared/api/client';
import { Mutations } from '@coopenomics/sdk';
async function generateDocument(data: IGenerateDocumentInput): Promise<IGeneratedDocumentOutput> {
const { [Mutations.Capital.GenerateGenerationAgreement.name]: result } =
await client.Mutation(Mutations.Capital.GenerateGenerationAgreement.mutation, {
const { [Mutations.Capital.GenerateGenerationContract.name]: result } =
await client.Mutation(Mutations.Capital.GenerateGenerationContract.mutation, {
variables: {
data: {
coopname: 'coopname',
+32 -18
View File
@@ -31,7 +31,7 @@ type Account {
private_account: PrivateAccount
"""
объект аккаунта в системе учёта провайдера, т.е. MONO. Здесь хранится приватная информация о пайщике кооператива, которая содержит его приватные данные. Эти данные не публикуются в блокчейне и не выходят за пределы базы данных провайдера. Они используются для заполнения шаблонов документов при нажатии соответствующих кнопок на платформе.
объект аккаунта в системе учёта провайдера, т.е. MONO. Здесь хранится приватная информация о пайщике кооператива, которая содержит его приватные данные. Эти данные не публикуются в блокчейне и не выходят за пределы базы данных провайдера. Они используются для заполнения шаблонов документов при нажатии соответствующих кнопок на платформе.
"""
provider_account: MonoAccount
@@ -2291,12 +2291,12 @@ input CapitalLogFilterInput {
type CapitalOnboardingState {
blagorost_offer_template_done: Boolean!
blagorost_provision_done: Boolean!
generation_agreement_template_done: Boolean!
generation_contract_template_done: Boolean!
generator_program_template_done: Boolean!
onboarding_blagorost_offer_template_hash: String
onboarding_blagorost_provision_hash: String
onboarding_expire_at: String!
onboarding_generation_agreement_template_hash: String
onboarding_generation_contract_template_hash: String
onboarding_generator_program_template_hash: String
onboarding_init_at: String!
}
@@ -2304,7 +2304,7 @@ type CapitalOnboardingState {
enum CapitalOnboardingStep {
blagorost_offer_template
blagorost_program
generation_agreement_template
generation_contract_template
generator_program_template
}
@@ -3581,7 +3581,7 @@ input CompleteVotingInput {
project_hash: String!
}
input ComponentGenerationAgreementGenerateDocumentInput {
input ComponentGenerationContractGenerateDocumentInput {
"""Номер блока, на котором был создан документ"""
block_num: Int
@@ -5536,7 +5536,7 @@ type GeneratedRegistrationDocument {
title: String!
}
input GenerationAgreementGenerateDocumentInput {
input GenerationContractGenerateDocumentInput {
"""Номер блока, на котором был создан документ"""
block_num: Int
@@ -5571,7 +5571,7 @@ input GenerationAgreementGenerateDocumentInput {
version: String
}
input GenerationAgreementSignedDocumentInput {
input GenerationContractSignedDocumentInput {
"""Хэш содержимого документа"""
doc_hash: String!
@@ -5581,7 +5581,7 @@ input GenerationAgreementSignedDocumentInput {
"""
Метаинформация для документа договора участия в хозяйственной деятельности
"""
meta: GenerationAgreementSignedMetaDocumentInput!
meta: GenerationContractSignedMetaDocumentInput!
"""Хэш мета-данных"""
meta_hash: String!
@@ -5593,7 +5593,7 @@ input GenerationAgreementSignedDocumentInput {
version: String!
}
input GenerationAgreementSignedMetaDocumentInput {
input GenerationContractSignedMetaDocumentInput {
"""Номер блока, на котором был создан документ"""
block_num: Int!
@@ -6551,7 +6551,7 @@ type Mutation {
capitalGenerateCapitalizationToMainWalletConvertStatement(data: GenerateDocumentInput!, options: GenerateDocumentOptionsInput): GeneratedDocument!
"""Сгенерировать документ дополнения к приложению для компонента"""
capitalGenerateComponentGenerationAgreement(data: ComponentGenerationAgreementGenerateDocumentInput!, options: GenerateDocumentOptionsInput): GeneratedDocument!
capitalGenerateComponentGenerationContract(data: ComponentGenerationContractGenerateDocumentInput!, options: GenerateDocumentOptionsInput): GeneratedDocument!
"""Сгенерировать решение о расходе"""
capitalGenerateExpenseDecision(data: GenerateDocumentInput!, options: GenerateDocumentOptionsInput): GeneratedDocument!
@@ -6560,7 +6560,7 @@ type Mutation {
capitalGenerateExpenseStatement(data: GenerateDocumentInput!, options: GenerateDocumentOptionsInput): GeneratedDocument!
"""Сгенерировать генерационное соглашение"""
capitalGenerateGenerationAgreement(data: GenerationAgreementGenerateDocumentInput!, options: GenerateDocumentOptionsInput): GeneratedDocument!
capitalGenerateGenerationContract(data: GenerationContractGenerateDocumentInput!, options: GenerateDocumentOptionsInput): GeneratedDocument!
"""Сгенерировать заявление об инвестировании в генерацию"""
capitalGenerateGenerationMoneyInvestStatement(data: GenerateDocumentInput!, options: GenerateDocumentOptionsInput): GeneratedDocument!
@@ -6593,7 +6593,7 @@ type Mutation {
capitalGenerateGetLoanStatement(data: GenerateDocumentInput!, options: GenerateDocumentOptionsInput): GeneratedDocument!
"""Сгенерировать документ приложения к договору участия для проекта"""
capitalGenerateProjectGenerationAgreement(data: ProjectGenerationAgreementGenerateDocumentInput!, options: GenerateDocumentOptionsInput): GeneratedDocument!
capitalGenerateProjectGenerationContract(data: ProjectGenerationContractGenerateDocumentInput!, options: GenerateDocumentOptionsInput): GeneratedDocument!
"""Сгенерировать акт о вкладе результатов"""
capitalGenerateResultContributionAct(data: GenerateDocumentInput!, options: GenerateDocumentOptionsInput): GeneratedDocument!
@@ -7872,6 +7872,12 @@ enum ProgramInvestStatus {
UNDEFINED
}
"""Ключ выбранной программы регистрации"""
enum ProgramKey {
CAPITALIZATION
GENERATION
}
input ProhibitRequestInput {
"""Имя аккаунта кооператива"""
coopname: String!
@@ -7979,7 +7985,7 @@ input ProjectFreeDecisionSignedMetaDocumentInput {
version: String!
}
input ProjectGenerationAgreementGenerateDocumentInput {
input ProjectGenerationContractGenerateDocumentInput {
"""Номер блока, на котором был создан документ"""
block_num: Int
@@ -8510,7 +8516,7 @@ input RegisterContributorInput {
about: String
"""Документ контракта"""
contract: GenerationAgreementSignedDocumentInput!
contract: GenerationContractSignedDocumentInput!
"""Хэш участника для верификации документа"""
contributor_hash: String!
@@ -8529,17 +8535,25 @@ input RegisterContributorInput {
}
input RegisterParticipantInput {
"""Имя кооперативного участка"""
braname: String
"""
Подписанный документ соглашения по капитализации (опционально, только если требуется)
"""
blagorost_offer: SignedDigitalDocumentInput
"""Имя кооперативного участка"""
braname: String
"""
Подписанный документ оферты по программе "Генератор" (опционально, только для программы generation)
"""
generator_offer: SignedDigitalDocumentInput
"""Подписанный документ политики конфиденциальности от пайщика"""
privacy_agreement: SignedDigitalDocumentInput!
"""Ключ выбранной программы регистрации"""
program_key: ProgramKey
"""Подписанный документ положения о цифровой подписи от пайщика"""
signature_agreement: SignedDigitalDocumentInput!
@@ -10221,4 +10235,4 @@ input WebPushSubscriptionKeysInput {
"""P256DH ключ для шифрования"""
p256dh: String!
}
}
@@ -9,10 +9,10 @@ import { SignedDigitalDocumentInputDTO } from '~/application/document/dto/signed
type ExcludeCommonProps<T> = Omit<T, 'coopname' | 'username' | 'registry_id' | 'block_num' | 'lang'>;
// интерфейс параметров для генерации
type action = Cooperative.Registry.ComponentGenerationAgreement.Action;
type action = Cooperative.Registry.ComponentGenerationContract.Action;
@InputType(`BaseComponentGenerationAgreementMetaDocumentInput`)
class BaseComponentGenerationAgreementMetaDocumentInputDTO implements ExcludeCommonProps<action> {
@InputType(`BaseComponentGenerationContractMetaDocumentInput`)
class BaseComponentGenerationContractMetaDocumentInputDTO implements ExcludeCommonProps<action> {
@Field({ description: 'Хэш компонента (проекта)' })
@IsString()
@IsNotEmpty()
@@ -24,17 +24,17 @@ class BaseComponentGenerationAgreementMetaDocumentInputDTO implements ExcludeCom
parent_project_hash!: string;
}
@InputType(`ComponentGenerationAgreementGenerateDocumentInput`)
export class ComponentGenerationAgreementGenerateDocumentInputDTO extends IntersectionType(
BaseComponentGenerationAgreementMetaDocumentInputDTO,
@InputType(`ComponentGenerationContractGenerateDocumentInput`)
export class ComponentGenerationContractGenerateDocumentInputDTO extends IntersectionType(
BaseComponentGenerationContractMetaDocumentInputDTO,
OmitType(GenerateMetaDocumentInputDTO, ['registry_id'] as const)
) {
registry_id!: number;
}
@InputType(`ComponentGenerationAgreementSignedMetaDocumentInput`)
export class ComponentGenerationAgreementSignedMetaDocumentInputDTO extends IntersectionType(
BaseComponentGenerationAgreementMetaDocumentInputDTO,
@InputType(`ComponentGenerationContractSignedMetaDocumentInput`)
export class ComponentGenerationContractSignedMetaDocumentInputDTO extends IntersectionType(
BaseComponentGenerationContractMetaDocumentInputDTO,
MetaDocumentInputDTO
) {
@Field({ description: 'Хэш дополнения к приложению (для компонента)' })
@@ -70,10 +70,10 @@ export class ComponentGenerationAgreementSignedMetaDocumentInputDTO extends Inte
project_id!: string;
}
@InputType(`ComponentGenerationAgreementSignedDocumentInput`)
export class ComponentGenerationAgreementSignedDocumentInputDTO extends SignedDigitalDocumentInputDTO {
@Field(() => ComponentGenerationAgreementSignedMetaDocumentInputDTO, {
@InputType(`ComponentGenerationContractSignedDocumentInput`)
export class ComponentGenerationContractSignedDocumentInputDTO extends SignedDigitalDocumentInputDTO {
@Field(() => ComponentGenerationContractSignedMetaDocumentInputDTO, {
description: 'Метаинформация для документа дополнения к приложению договора участия в хозяйственной деятельности для компонентов',
})
public readonly meta!: ComponentGenerationAgreementSignedMetaDocumentInputDTO;
public readonly meta!: ComponentGenerationContractSignedMetaDocumentInputDTO;
}
@@ -9,18 +9,18 @@ import { SignedDigitalDocumentInputDTO } from '~/application/document/dto/signed
type ExcludeCommonProps<T> = Omit<T, 'coopname' | 'username' | 'registry_id'>;
// интерфейс параметров для генерации
type action = Cooperative.Registry.GenerationAgreement.Action;
type action = Cooperative.Registry.GenerationContract.Action;
@InputType(`BaseGenerationAgreementMetaDocumentInput`)
class BaseGenerationAgreementMetaDocumentInputDTO implements ExcludeCommonProps<action> {
@InputType(`BaseGenerationContractMetaDocumentInput`)
class BaseGenerationContractMetaDocumentInputDTO implements ExcludeCommonProps<action> {
@Field({ description: 'Хэш участника для генерации соглашения' })
@IsString()
contributor_hash!: string;
}
@InputType(`GenerationAgreementGenerateDocumentInput`)
export class GenerationAgreementGenerateDocumentInputDTO extends IntersectionType(
BaseGenerationAgreementMetaDocumentInputDTO,
@InputType(`GenerationContractGenerateDocumentInput`)
export class GenerationContractGenerateDocumentInputDTO extends IntersectionType(
BaseGenerationContractMetaDocumentInputDTO,
OmitType(GenerateMetaDocumentInputDTO, ['registry_id'] as const)
) {
registry_id!: number;
@@ -30,15 +30,15 @@ export class GenerationAgreementGenerateDocumentInputDTO extends IntersectionTyp
}
}
@InputType(`GenerationAgreementSignedMetaDocumentInput`)
export class GenerationAgreementSignedMetaDocumentInputDTO
extends IntersectionType(BaseGenerationAgreementMetaDocumentInputDTO, MetaDocumentInputDTO)
@InputType(`GenerationContractSignedMetaDocumentInput`)
export class GenerationContractSignedMetaDocumentInputDTO
extends IntersectionType(BaseGenerationContractMetaDocumentInputDTO, MetaDocumentInputDTO)
implements action {}
@InputType(`GenerationAgreementSignedDocumentInput`)
export class GenerationAgreementSignedDocumentInputDTO extends SignedDigitalDocumentInputDTO {
@Field(() => GenerationAgreementSignedMetaDocumentInputDTO, {
@InputType(`GenerationContractSignedDocumentInput`)
export class GenerationContractSignedDocumentInputDTO extends SignedDigitalDocumentInputDTO {
@Field(() => GenerationContractSignedMetaDocumentInputDTO, {
description: 'Метаинформация для документа договора участия в хозяйственной деятельности',
})
public readonly meta!: GenerationAgreementSignedMetaDocumentInputDTO;
public readonly meta!: GenerationContractSignedMetaDocumentInputDTO;
}
@@ -9,27 +9,27 @@ import { SignedDigitalDocumentInputDTO } from '~/application/document/dto/signed
type ExcludeCommonProps<T> = Omit<T, 'coopname' | 'username' | 'registry_id' | 'block_num' | 'lang'>;
// интерфейс параметров для генерации
type action = Cooperative.Registry.ProjectGenerationAgreement.Action;
type action = Cooperative.Registry.ProjectGenerationContract.Action;
@InputType(`BaseProjectGenerationAgreementMetaDocumentInput`)
class BaseProjectGenerationAgreementMetaDocumentInputDTO implements ExcludeCommonProps<action> {
@InputType(`BaseProjectGenerationContractMetaDocumentInput`)
class BaseProjectGenerationContractMetaDocumentInputDTO implements ExcludeCommonProps<action> {
@Field({ description: 'Хэш проекта' })
@IsString()
@IsNotEmpty()
project_hash!: string;
}
@InputType(`ProjectGenerationAgreementGenerateDocumentInput`)
export class ProjectGenerationAgreementGenerateDocumentInputDTO extends IntersectionType(
BaseProjectGenerationAgreementMetaDocumentInputDTO,
@InputType(`ProjectGenerationContractGenerateDocumentInput`)
export class ProjectGenerationContractGenerateDocumentInputDTO extends IntersectionType(
BaseProjectGenerationContractMetaDocumentInputDTO,
OmitType(GenerateMetaDocumentInputDTO, ['registry_id'] as const)
) {
registry_id!: number;
}
@InputType(`ProjectGenerationAgreementSignedMetaDocumentInput`)
export class ProjectGenerationAgreementSignedMetaDocumentInputDTO extends IntersectionType(
BaseProjectGenerationAgreementMetaDocumentInputDTO,
@InputType(`ProjectGenerationContractSignedMetaDocumentInput`)
export class ProjectGenerationContractSignedMetaDocumentInputDTO extends IntersectionType(
BaseProjectGenerationContractMetaDocumentInputDTO,
MetaDocumentInputDTO
) {
@Field({ description: 'Хэш приложения' })
@@ -53,10 +53,10 @@ export class ProjectGenerationAgreementSignedMetaDocumentInputDTO extends Inters
project_id!: string;
}
@InputType(`ProjectGenerationAgreementSignedDocumentInput`)
export class ProjectGenerationAgreementSignedDocumentInputDTO extends SignedDigitalDocumentInputDTO {
@Field(() => ProjectGenerationAgreementSignedMetaDocumentInputDTO, {
@InputType(`ProjectGenerationContractSignedDocumentInput`)
export class ProjectGenerationContractSignedDocumentInputDTO extends SignedDigitalDocumentInputDTO {
@Field(() => ProjectGenerationContractSignedMetaDocumentInputDTO, {
description: 'Метаинформация для документа приложения к договору участия в хозяйственной деятельности для проектов',
})
public readonly meta!: ProjectGenerationAgreementSignedMetaDocumentInputDTO;
public readonly meta!: ProjectGenerationContractSignedMetaDocumentInputDTO;
}
@@ -25,9 +25,9 @@ export class CooperativeConfigService {
* Получить дополнительные соглашения по умолчанию для кооператива
*/
getDefaultAdditionalAgreements(coopname: string, accountType: AccountType): AgreementId[] {
if (coopname === 'voskhod' && accountType === AccountType.individual) {
return [AgreementId.BLAGOROST_OFFER];
}
// if (coopname === 'voskhod' && accountType === AccountType.individual) {
// return [AgreementId.BLAGOROST_OFFER];
// }
return [];
}
@@ -3,7 +3,7 @@ import { IsEnum, IsNotEmpty, IsOptional, IsString, MaxLength } from 'class-valid
export enum CapitalOnboardingStepEnum {
generator_program_template = 'generator_program_template',
generation_agreement_template = 'generation_agreement_template',
generation_contract_template = 'generation_contract_template',
blagorost_program = 'blagorost_program',
blagorost_offer_template = 'blagorost_offer_template',
}
@@ -44,10 +44,10 @@ export class CapitalOnboardingStateDTO {
onboarding_generator_program_template_hash?: string | null;
@Field(() => Boolean)
generation_agreement_template_done!: boolean;
generation_contract_template_done!: boolean;
@Field(() => String, { nullable: true })
onboarding_generation_agreement_template_hash?: string | null;
onboarding_generation_contract_template_hash?: string | null;
@Field(() => Boolean)
blagorost_provision_done!: boolean;
@@ -2,7 +2,7 @@ import { Field, InputType } from '@nestjs/graphql';
import { IsNotEmpty, IsNumber, IsOptional, IsString } from 'class-validator';
import { Type } from 'class-transformer';
import type { RegisterContributorDomainInput } from '../../../domain/actions/register-contributor-domain-input.interface';
import { GenerationAgreementSignedDocumentInputDTO } from '~/application/document/documents-dto/generation-agreement-document.dto';
import { GenerationContractSignedDocumentInputDTO } from '~/application/document/documents-dto/generation-agreement-document.dto';
/**
* GraphQL DTO для регистрации участника CAPITAL контракта
@@ -40,7 +40,7 @@ export class RegisterContributorInputDTO implements RegisterContributorDomainInp
@Type(() => Number)
hours_per_day?: number;
@Field(() => GenerationAgreementSignedDocumentInputDTO, { description: 'Документ контракта' })
@Type(() => GenerationAgreementSignedDocumentInputDTO)
contract!: GenerationAgreementSignedDocumentInputDTO;
@Field(() => GenerationContractSignedDocumentInputDTO, { description: 'Документ контракта' })
@Type(() => GenerationContractSignedDocumentInputDTO)
contract!: GenerationContractSignedDocumentInputDTO;
}
@@ -16,9 +16,9 @@ import { GetContributorInputDTO } from '../dto/participation_management/get-cont
import { createPaginationResult, PaginationInputDTO, PaginationResult } from '~/application/common/dto/pagination.dto';
import { GeneratedDocumentDTO } from '~/application/document/dto/generated-document.dto';
import { GenerateDocumentOptionsInputDTO } from '~/application/document/dto/generate-document-options-input.dto';
import { GenerationAgreementGenerateDocumentInputDTO } from '~/application/document/documents-dto/generation-agreement-document.dto';
import { ProjectGenerationAgreementGenerateDocumentInputDTO } from '~/application/document/documents-dto/project-generation-agreement-document.dto';
import { ComponentGenerationAgreementGenerateDocumentInputDTO } from '~/application/document/documents-dto/component-generation-agreement-document.dto';
import { GenerationContractGenerateDocumentInputDTO } from '~/application/document/documents-dto/generation-agreement-document.dto';
import { ProjectGenerationContractGenerateDocumentInputDTO } from '~/application/document/documents-dto/project-generation-agreement-document.dto';
import { ComponentGenerationContractGenerateDocumentInputDTO } from '~/application/document/documents-dto/component-generation-agreement-document.dto';
import { GenerateDocumentInputDTO } from '~/application/document/dto/generate-document-input.dto';
/**
@@ -155,56 +155,56 @@ export class ParticipationManagementResolver {
* Мутация для генерации генерационного соглашения
*/
@Mutation(() => GeneratedDocumentDTO, {
name: 'capitalGenerateGenerationAgreement',
name: 'capitalGenerateGenerationContract',
description: 'Сгенерировать генерационное соглашение',
})
@Throttle({ default: { limit: 3, ttl: 60000 } })
@UseGuards(GqlJwtAuthGuard, RolesGuard)
@AuthRoles(['chairman', 'member'])
async generateGenerationAgreement(
@Args('data', { type: () => GenerationAgreementGenerateDocumentInputDTO })
data: GenerationAgreementGenerateDocumentInputDTO,
async generateGenerationContract(
@Args('data', { type: () => GenerationContractGenerateDocumentInputDTO })
data: GenerationContractGenerateDocumentInputDTO,
@Args('options', { type: () => GenerateDocumentOptionsInputDTO, nullable: true })
options: GenerateDocumentOptionsInputDTO
): Promise<GeneratedDocumentDTO> {
return this.participationManagementService.generateGenerationAgreement(data, options);
return this.participationManagementService.generateGenerationContract(data, options);
}
/**
* Мутация для генерации документа приложения к договору участия для проекта (1002)
*/
@Mutation(() => GeneratedDocumentDTO, {
name: 'capitalGenerateProjectGenerationAgreement',
name: 'capitalGenerateProjectGenerationContract',
description: 'Сгенерировать документ приложения к договору участия для проекта',
})
@Throttle({ default: { limit: 3, ttl: 60000 } })
@UseGuards(GqlJwtAuthGuard, RolesGuard)
@AuthRoles(['chairman', 'member'])
async generateProjectGenerationAgreement(
@Args('data', { type: () => ProjectGenerationAgreementGenerateDocumentInputDTO })
data: ProjectGenerationAgreementGenerateDocumentInputDTO,
async generateProjectGenerationContract(
@Args('data', { type: () => ProjectGenerationContractGenerateDocumentInputDTO })
data: ProjectGenerationContractGenerateDocumentInputDTO,
@Args('options', { type: () => GenerateDocumentOptionsInputDTO, nullable: true })
options: GenerateDocumentOptionsInputDTO
): Promise<GeneratedDocumentDTO> {
return this.participationManagementService.generateProjectGenerationAgreement(data, options);
return this.participationManagementService.generateProjectGenerationContract(data, options);
}
/**
* Мутация для генерации документа дополнения к приложению для компонента (1003)
*/
@Mutation(() => GeneratedDocumentDTO, {
name: 'capitalGenerateComponentGenerationAgreement',
name: 'capitalGenerateComponentGenerationContract',
description: 'Сгенерировать документ дополнения к приложению для компонента',
})
@Throttle({ default: { limit: 3, ttl: 60000 } })
@UseGuards(GqlJwtAuthGuard, RolesGuard)
@AuthRoles(['chairman', 'member'])
async generateComponentGenerationAgreement(
@Args('data', { type: () => ComponentGenerationAgreementGenerateDocumentInputDTO })
data: ComponentGenerationAgreementGenerateDocumentInputDTO,
async generateComponentGenerationContract(
@Args('data', { type: () => ComponentGenerationContractGenerateDocumentInputDTO })
data: ComponentGenerationContractGenerateDocumentInputDTO,
@Args('options', { type: () => GenerateDocumentOptionsInputDTO, nullable: true })
options: GenerateDocumentOptionsInputDTO
): Promise<GeneratedDocumentDTO> {
return this.participationManagementService.generateComponentGenerationAgreement(data, options);
return this.participationManagementService.generateComponentGenerationContract(data, options);
}
}
@@ -64,7 +64,7 @@ export class CapitalOnboardingEventsService {
private mapStepToFlag(step: string): keyof IConfig | null {
const mapping: Record<string, keyof IConfig> = {
generator_program_template: 'onboarding_generator_program_template_done',
generation_agreement_template: 'onboarding_generation_agreement_template_done',
generation_contract_template: 'onboarding_generation_contract_template_done',
blagorost_program: 'onboarding_blagorost_provision_done',
blagorost_offer_template: 'onboarding_blagorost_offer_template_done',
};
@@ -16,13 +16,13 @@ import { DecisionEventType } from '~/domain/decision-tracking/interfaces/trackin
type OnboardingFlagKey =
| 'onboarding_generator_program_template_done'
| 'onboarding_generation_agreement_template_done'
| 'onboarding_generation_contract_template_done'
| 'onboarding_blagorost_provision_done'
| 'onboarding_blagorost_offer_template_done';
type OnboardingHashKey =
| 'onboarding_generator_program_template_hash'
| 'onboarding_generation_agreement_template_hash'
| 'onboarding_generation_contract_template_hash'
| 'onboarding_blagorost_provision_hash'
| 'onboarding_blagorost_offer_template_hash';
@@ -38,8 +38,8 @@ export class CapitalOnboardingService {
switch (step) {
case CapitalOnboardingStepEnum.generator_program_template:
return 'onboarding_generator_program_template_done';
case CapitalOnboardingStepEnum.generation_agreement_template:
return 'onboarding_generation_agreement_template_done';
case CapitalOnboardingStepEnum.generation_contract_template:
return 'onboarding_generation_contract_template_done';
case CapitalOnboardingStepEnum.blagorost_program:
return 'onboarding_blagorost_provision_done';
case CapitalOnboardingStepEnum.blagorost_offer_template:
@@ -53,8 +53,8 @@ export class CapitalOnboardingService {
switch (step) {
case CapitalOnboardingStepEnum.generator_program_template:
return 'onboarding_generator_program_template_hash';
case CapitalOnboardingStepEnum.generation_agreement_template:
return 'onboarding_generation_agreement_template_hash';
case CapitalOnboardingStepEnum.generation_contract_template:
return 'onboarding_generation_contract_template_hash';
case CapitalOnboardingStepEnum.blagorost_program:
return 'onboarding_blagorost_provision_hash';
case CapitalOnboardingStepEnum.blagorost_offer_template:
@@ -68,8 +68,8 @@ export class CapitalOnboardingService {
switch (step) {
case CapitalOnboardingStepEnum.generator_program_template:
return 'generator_program';
case CapitalOnboardingStepEnum.generation_agreement_template:
return 'generation_agreement_template';
case CapitalOnboardingStepEnum.generation_contract_template:
return 'generation_contract_template';
case CapitalOnboardingStepEnum.blagorost_program:
return 'blagorost_program';
case CapitalOnboardingStepEnum.blagorost_offer_template:
@@ -109,8 +109,8 @@ export class CapitalOnboardingService {
return {
generator_program_template_done: !!pluginConfig.onboarding_generator_program_template_done,
onboarding_generator_program_template_hash: pluginConfig.onboarding_generator_program_template_hash || null,
generation_agreement_template_done: !!pluginConfig.onboarding_generation_agreement_template_done,
onboarding_generation_agreement_template_hash: pluginConfig.onboarding_generation_agreement_template_hash || null,
generation_contract_template_done: !!pluginConfig.onboarding_generation_contract_template_done,
onboarding_generation_contract_template_hash: pluginConfig.onboarding_generation_contract_template_hash || null,
blagorost_provision_done: !!pluginConfig.onboarding_blagorost_provision_done,
onboarding_blagorost_provision_hash: pluginConfig.onboarding_blagorost_provision_hash || null,
blagorost_offer_template_done: !!pluginConfig.onboarding_blagorost_offer_template_done,
@@ -17,9 +17,9 @@ import { DocumentInteractor } from '~/application/document/interactors/document.
import { ContributorMapperService } from './contributor-mapper.service';
import { ContributorSyncService } from '../syncers/contributor-sync.service';
import { Cooperative } from 'cooptypes';
import { GenerationAgreementGenerateDocumentInputDTO } from '~/application/document/documents-dto/generation-agreement-document.dto';
import { ProjectGenerationAgreementGenerateDocumentInputDTO } from '~/application/document/documents-dto/project-generation-agreement-document.dto';
import { ComponentGenerationAgreementGenerateDocumentInputDTO } from '~/application/document/documents-dto/component-generation-agreement-document.dto';
import { GenerationContractGenerateDocumentInputDTO } from '~/application/document/documents-dto/generation-agreement-document.dto';
import { ProjectGenerationContractGenerateDocumentInputDTO } from '~/application/document/documents-dto/project-generation-agreement-document.dto';
import { ComponentGenerationContractGenerateDocumentInputDTO } from '~/application/document/documents-dto/component-generation-agreement-document.dto';
/**
* Сервис уровня приложения для управления участием в CAPITAL
@@ -53,21 +53,21 @@ export class ParticipationManagementService {
/**
* Генерация документа приложения к договору участия для проекта (1002)
*/
async generateProjectGenerationAgreement(
data: ProjectGenerationAgreementGenerateDocumentInputDTO,
async generateProjectGenerationContract(
data: ProjectGenerationContractGenerateDocumentInputDTO,
options?: GenerateDocumentOptionsInputDTO
): Promise<GeneratedDocumentDTO> {
return await this.participationManagementInteractor.generateProjectGenerationAgreement(data, options);
return await this.participationManagementInteractor.generateProjectGenerationContract(data, options);
}
/**
* Генерация документа дополнения к приложению для компонента (1003)
*/
async generateComponentGenerationAgreement(
data: ComponentGenerationAgreementGenerateDocumentInputDTO,
async generateComponentGenerationContract(
data: ComponentGenerationContractGenerateDocumentInputDTO,
options?: GenerateDocumentOptionsInputDTO
): Promise<GeneratedDocumentDTO> {
return await this.participationManagementInteractor.generateComponentGenerationAgreement(data, options);
return await this.participationManagementInteractor.generateComponentGenerationContract(data, options);
}
/**
@@ -171,14 +171,14 @@ export class ParticipationManagementService {
/**
* Генерация генерационного соглашения
*/
async generateGenerationAgreement(
data: GenerationAgreementGenerateDocumentInputDTO,
async generateGenerationContract(
data: GenerationContractGenerateDocumentInputDTO,
options: GenerateDocumentOptionsInputDTO
): Promise<GeneratedDocumentDTO> {
const document = await this.documentInteractor.generateDocument({
data: {
...data,
registry_id: Cooperative.Registry.GenerationAgreement.registry_id,
registry_id: Cooperative.Registry.GenerationContract.registry_id,
},
options,
});
@@ -27,8 +27,8 @@ import type {
PaginationInputDomainInterface,
PaginationResultDomainInterface,
} from '~/domain/common/interfaces/pagination.interface';
import type { ProjectGenerationAgreementGenerateDocumentInputDTO } from '~/application/document/documents-dto/project-generation-agreement-document.dto';
import type { ComponentGenerationAgreementGenerateDocumentInputDTO } from '~/application/document/documents-dto/component-generation-agreement-document.dto';
import type { ProjectGenerationContractGenerateDocumentInputDTO } from '~/application/document/documents-dto/project-generation-agreement-document.dto';
import type { ComponentGenerationContractGenerateDocumentInputDTO } from '~/application/document/documents-dto/component-generation-agreement-document.dto';
import type { GenerateDocumentOptionsInputDTO } from '~/application/document/dto/generate-document-options-input.dto';
import type { GeneratedDocumentDTO } from '~/application/document/dto/generated-document.dto';
import { DomainToBlockchainUtils } from '~/shared/utils/domain-to-blockchain.utils';
@@ -362,8 +362,8 @@ export class ParticipationManagementInteractor {
/**
* Генерация документа приложения к договору участия для проекта (1002)
*/
async generateProjectGenerationAgreement(
data: ProjectGenerationAgreementGenerateDocumentInputDTO,
async generateProjectGenerationContract(
data: ProjectGenerationContractGenerateDocumentInputDTO,
options?: GenerateDocumentOptionsInputDTO
): Promise<GeneratedDocumentDTO> {
// 1. Получаем данные участника
@@ -398,7 +398,7 @@ export class ParticipationManagementInteractor {
if (isComponent) {
throw new HttpApiError(
httpStatus.BAD_REQUEST,
`Проект ${data.project_hash} является компонентом. Используйте generateComponentGenerationAgreement`
`Проект ${data.project_hash} является компонентом. Используйте generateComponentGenerationContract`
);
}
@@ -410,7 +410,7 @@ export class ParticipationManagementInteractor {
coopname: data.coopname,
username: data.username,
lang: data.lang || 'ru',
registry_id: Cooperative.Registry.ProjectGenerationAgreement.registry_id,
registry_id: Cooperative.Registry.ProjectGenerationContract.registry_id,
appendix_hash,
contributor_hash: contributor.contributor_hash,
contributor_created_at: contributor.created_at,
@@ -430,8 +430,8 @@ export class ParticipationManagementInteractor {
/**
* Генерация документа дополнения к приложению для компонента (1003)
*/
async generateComponentGenerationAgreement(
data: ComponentGenerationAgreementGenerateDocumentInputDTO,
async generateComponentGenerationContract(
data: ComponentGenerationContractGenerateDocumentInputDTO,
options?: GenerateDocumentOptionsInputDTO
): Promise<GeneratedDocumentDTO> {
// 1. Получаем данные участника
@@ -466,7 +466,7 @@ export class ParticipationManagementInteractor {
if (!isComponent) {
throw new HttpApiError(
httpStatus.BAD_REQUEST,
`Проект ${data.component_hash} не является компонентом. Используйте generateProjectGenerationAgreement`
`Проект ${data.component_hash} не является компонентом. Используйте generateProjectGenerationContract`
);
}
@@ -511,7 +511,7 @@ export class ParticipationManagementInteractor {
coopname: data.coopname,
username: data.username,
lang: data.lang || 'ru',
registry_id: Cooperative.Registry.ComponentGenerationAgreement.registry_id,
registry_id: Cooperative.Registry.ComponentGenerationContract.registry_id,
component_appendix_hash,
parent_appendix_hash: parentAppendix.appendix_hash,
contributor_hash: contributor.contributor_hash,
@@ -35,7 +35,7 @@ export const defaultConfig = {
energy_gain_coefficient: 1.0,
// Онбординг флаги
onboarding_generator_program_template_done: false,
onboarding_generation_agreement_template_done: false,
onboarding_generation_contract_template_done: false,
onboarding_blagorost_provision_done: false,
onboarding_blagorost_offer_template_done: false,
} as const;
@@ -159,9 +159,9 @@ export const Schema = z.object({
.boolean()
.default(defaultConfig.onboarding_generator_program_template_done)
.describe(describeField({ label: 'Шаг положения о программе ГЕНЕРАТОР выполнен', visible: false })),
onboarding_generation_agreement_template_done: z
onboarding_generation_contract_template_done: z
.boolean()
.default(defaultConfig.onboarding_generation_agreement_template_done)
.default(defaultConfig.onboarding_generation_contract_template_done)
.describe(describeField({ label: 'Шаг соглашения о генерации выполнен', visible: false })),
onboarding_blagorost_provision_done: z
.boolean()
@@ -89,7 +89,7 @@ export interface IVars {
protocol_number: string
protocol_day_month_year: string
}
generation_agreement_template?: { // Шаблон договора участия в хозяйственной деятельности
generation_contract_template?: { // Шаблон договора участия в хозяйственной деятельности
protocol_number: string
protocol_day_month_year: string
}
@@ -33,7 +33,7 @@ export interface Model {
export const title = 'Приложение к договору участия в проекте'
export const description = 'Приложение к договору участия в хозяйственной деятельности для проектов'
export const context = `<div class="digital-document"><div style="text-align: center"><h1>ПРИЛОЖЕНИЕ № {{ short_appendix_hash }}</h1><h2>к Договору об участии в хозяйственной деятельности № УХД-{{ short_contributor_hash }}</h2></div><p style="text-align: right">{{ meta.created_at }}, {{ coop.city }}</p><p>{% trans 'parties_intro' %} "{{ vars.name }}" {% trans 'in_face_of_chairman' %} {{ coop.chairman.last_name }} {{ coop.chairman.first_name }} {{ coop.chairman.middle_name }}, {% trans 'acting_on_basis_of_charter' %}, {% trans 'hereinafter_referred_to_as' %} "{% trans 'society' %}", {% trans 'and_participant' %} {{ user.full_name_or_short_name }}, {% trans 'hereinafter_referred_to_as' %} "{% trans 'participant' %}", {% trans 'jointly_referred_to_as' %} "{% trans 'parties' %}", {% trans 'have_concluded_this_appendix' %} {% trans 'hereinafter_referred_to_as' %} "{% trans 'appendix' %}" {% trans 'to_agreement' %} УХД-{{ short_contributor_hash }} {% trans 'from_date' %} {{ contributor_created_at }} {% trans 'of_the_following' %}:</p><p>{% trans 'according_to_regulations' %} "{{ vars.generation_agreement_template.protocol_number }}" {% trans 'from_date' %} {{ vars.generation_agreement_template.protocol_day_month_year }}), {% trans 'participant_commits_project' %} "{{ project_name }}" (№{{ project_id }}).</p><h2>{% trans 'details_and_signatures_of_parties' %}</h2><p><strong>{% trans 'society' %}/{{ vars.full_abbr }} "{{ vars.name }}"/:</strong></p><p>ИНН {{ coop.details.inn }}, КПП {{ coop.details.kpp }}, ОГРН {{ coop.details.ogrn }}</p><p>{% trans 'legal_address' %}: {{ coop.full_address }}</p><p>{% trans 'contact_phone' %}: {{ coop.phone }}</p><p>{% trans 'email' %}: {{ coop.email }}</p><p>{% trans 'bank_account' %}: {{ coop.defaultBankAccount.account_number }}</p><p>{% trans 'bank_name' %}: {{ coop.defaultBankAccount.bank_name }}</p><p>{% trans 'bik' %}: {{ coop.defaultBankAccount.details.bik }}</p><p>{% trans 'correspondent_account' %}: {{ coop.defaultBankAccount.details.corr }}</p><p>{% trans 'chairman' %} {{ vars.full_abbr_genitive }} "{{ vars.name }}"</p><p>{{ coop.chairman.last_name }} {{ coop.chairman.first_name }} {{ coop.chairman.middle_name }}</p><p>{% trans 'signed_by_digital_signature' %}</p><p><strong>{% trans 'participant' %}:</strong></p><p>{{ user.full_name_or_short_name }}</p><p>{% trans 'contact_phone' %}: {{ user.phone }}</p><p>{% trans 'email' %}: {{ user.email }}</p><p>{% trans 'signed_by_digital_signature' %}</p></div><style>.digital-document {padding: 20px;white-space: pre-wrap;}</style>`
export const context = `<div class="digital-document"><div style="text-align: center"><h1>ПРИЛОЖЕНИЕ № {{ short_appendix_hash }}</h1><h2>к Договору об участии в хозяйственной деятельности № УХД-{{ short_contributor_hash }}</h2></div><p style="text-align: right">{{ meta.created_at }}, {{ coop.city }}</p><p>{% trans 'parties_intro' %} "{{ vars.name }}" {% trans 'in_face_of_chairman' %} {{ coop.chairman.last_name }} {{ coop.chairman.first_name }} {{ coop.chairman.middle_name }}, {% trans 'acting_on_basis_of_charter' %}, {% trans 'hereinafter_referred_to_as' %} "{% trans 'society' %}", {% trans 'and_participant' %} {{ user.full_name_or_short_name }}, {% trans 'hereinafter_referred_to_as' %} "{% trans 'participant' %}", {% trans 'jointly_referred_to_as' %} "{% trans 'parties' %}", {% trans 'have_concluded_this_appendix' %} {% trans 'hereinafter_referred_to_as' %} "{% trans 'appendix' %}" {% trans 'to_agreement' %} УХД-{{ short_contributor_hash }} {% trans 'from_date' %} {{ contributor_created_at }} {% trans 'of_the_following' %}:</p><p>{% trans 'according_to_regulations' %} "{{ vars.generation_contract_template.protocol_number }}" {% trans 'from_date' %} {{ vars.generation_contract_template.protocol_day_month_year }}), {% trans 'participant_commits_project' %} "{{ project_name }}" (№{{ project_id }}).</p><h2>{% trans 'details_and_signatures_of_parties' %}</h2><p><strong>{% trans 'society' %}/{{ vars.full_abbr }} "{{ vars.name }}"/:</strong></p><p>ИНН {{ coop.details.inn }}, КПП {{ coop.details.kpp }}, ОГРН {{ coop.details.ogrn }}</p><p>{% trans 'legal_address' %}: {{ coop.full_address }}</p><p>{% trans 'contact_phone' %}: {{ coop.phone }}</p><p>{% trans 'email' %}: {{ coop.email }}</p><p>{% trans 'bank_account' %}: {{ coop.defaultBankAccount.account_number }}</p><p>{% trans 'bank_name' %}: {{ coop.defaultBankAccount.bank_name }}</p><p>{% trans 'bik' %}: {{ coop.defaultBankAccount.details.bik }}</p><p>{% trans 'correspondent_account' %}: {{ coop.defaultBankAccount.details.corr }}</p><p>{% trans 'chairman' %} {{ vars.full_abbr_genitive }} "{{ vars.name }}"</p><p>{{ coop.chairman.last_name }} {{ coop.chairman.first_name }} {{ coop.chairman.middle_name }}</p><p>{% trans 'signed_by_digital_signature' %}</p><p><strong>{% trans 'participant' %}:</strong></p><p>{{ user.full_name_or_short_name }}</p><p>{% trans 'contact_phone' %}: {{ user.phone }}</p><p>{% trans 'email' %}: {{ user.email }}</p><p>{% trans 'signed_by_digital_signature' %}</p></div><style>.digital-document {padding: 20px;white-space: pre-wrap;}</style>`
export const translations = {
ru: {
@@ -78,7 +78,7 @@ export const exampleData = {
project_name: 'Проект цифровой платформы',
project_id: 'B2C3D4E5F6789ABC',
vars: {
generation_agreement_template: {
generation_contract_template: {
protocol_number: 'СС-11-04-24',
protocol_day_month_year: '11 апреля 2024 г.',
},
@@ -92,7 +92,7 @@ export const exampleData = {
project_name: 'Проект цифровой платформы',
project_id: 'B2C3D4E5F6789ABC',
vars: {
generation_agreement_template: {
generation_contract_template: {
protocol_number: 'СС-11-04-24',
protocol_day_month_year: '11 апреля 2024 г.',
},
@@ -29,10 +29,10 @@ export * as GeneratorOffer from './996.GeneratorOffer'
export * as BlagorostOffer from './1000.BlagorostOffer'
export * as BlagorostProgramTemplate from './998.BlagorostProgramTemplate'
export * as BlagorostOfferTemplate from './999.BlagorostOfferTemplate'
export * as GenerationAgreementTemplate from './997.GenerationAgreementTemplate'
export * as GenerationAgreement from './1001.GenerationAgreement'
export * as ProjectGenerationAgreement from './1002.ProjectGenerationAgreement'
export * as ComponentGenerationAgreement from './1003.ComponentGenerationAgreement'
export * as GenerationContractTemplate from './997.GenerationContractTemplate'
export * as GenerationContract from './1001.GenerationContract'
export * as ProjectGenerationContract from './1002.ProjectGenerationContract'
export * as ComponentGenerationContract from './1003.ComponentGenerationContract'
export * as InitProjectStatement from './1005.InitProjectStatement'
export * as InitProjectDecision from './1006.InitProjectDecision'
@@ -1,90 +0,0 @@
---
description: Правило реализации диалогов подтверждения для действий
alwaysApply: false
---
## Правило реализации диалогов подтверждения
### Принцип
Диалоги подтверждения должны реализовываться в UI компонентах кнопок, а не в composables функций моделей. Composable отвечает только за бизнес-логику выполнения действия.
### Структура реализации
```vue
<template lang="pug">
q-btn(
@click='showDialog = true',
:loading='isSubmitting'
)
// иконка действия
q-dialog(v-model='showDialog', @hide='close')
ModalBase(title='Заголовок действия')
Form.q-pa-sm(
:handler-submit='handleAction',
:is-submitting='isSubmitting',
:button-cancel-txt='"Отменить"',
:button-submit-txt='"Выполнить"',
@cancel='close'
)
div(style='max-width: 400px')
p Подтверждающее сообщение
// дополнительные поля ввода если нужны
</template>
<script lang="ts" setup>
// Импорты
import { FailAlert, SuccessAlert } from 'src/shared/api';
import { useModelFunction } from '../model';
import { ref } from 'vue';
import { ModalBase } from 'src/shared/ui/ModalBase';
import { Form } from 'src/shared/ui/Form';
// Props, emits
const emit = defineEmits(['actionCompleted', 'close']);
// Состояние
const { actionFunction } = useModelFunction();
const isSubmitting = ref(false);
const showDialog = ref(false);
// Важно: composable useModelFunction НЕ содержит try-catch!
// Он просто выполняет действие и возвращает результат.
// Вся обработка ошибок происходит здесь, в UI компоненте.
// Функции
const close = () => {
showDialog.value = false;
emit('close');
};
const handleAction = async () => {
isSubmitting.value = true;
try {
await actionFunction(params);
SuccessAlert('Действие выполнено');
emit('actionCompleted');
close();
} catch (e: any) {
FailAlert(`Возникла ошибка: ${e.message}`);
close();
} finally {
isSubmitting.value = false;
}
};
</script>
```
### Ключевые моменты
- **Диалог в компоненте**: Не в composable, а в UI компоненте
- **Состояние в компоненте**: `showDialog`, `isSubmitting` управляются локально
- **Эмиссия событий**: Для внешней обработки результатов действия
- **Composable без try-catch**: Функции модели НЕ содержат обработку ошибок, только выполнение действия
- **Уведомления**: Использовать `FailAlert`/`SuccessAlert` вместо прямых вызовов Notify
- **Валидация**: Проверять данные перед отправкой в composable
- **Очистка состояния**: Сбрасывать поля при закрытии диалога
- **Нет window.location.reload()**: Обновление данных через store или emits
### Примеры применения
- Удаление объектов (DeleteStoryButton.vue)
- Подтверждение действий (ConfirmApprovalButton.vue)
- Отклонение с причиной (DeclineApprovalButton.vue)
@@ -1,253 +0,0 @@
---
globs: ["**/entities/**/*.ts", "**/entities/**/*.vue"]
alwaysApply: false
---
## Архитектура сущностей (Entities Layer)
### Общее устройство
Слой сущностей содержит бизнес-сущности приложения и их модели данных. Каждая сущность имеет строгую структуру и отвечает только за хранение и управление данными.
**Запрещено**: Размещать методы действий (мутаций) в сущностях. Действия должны быть в фичах (features).
### Структура папок сущности
```
entities/
EntityName/
index.ts # Экспорт модели и API
api/
index.ts # Функции для работы с GraphQL API
model/
index.ts # Экспорт типов и store
types.ts # Типы сущностей на основе SDK
store.ts # Pinia store для реактивного состояния
```
### Организация типов (model/types.ts)
#### Импорты
```typescript
import type { Queries, Mutations, Zeus } from '@coopenomics/sdk';
```
#### Паттерны типизации
**Для простых сущностей:**
```typescript
// Тип пагинированного списка
export type IEntitiesPagination =
Queries.Extension.GetEntities.IOutput[typeof Queries.Extension.GetEntities.name];
// Тип одной сущности
export type IEntity = IEntitiesPagination['items'][0];
// Типы входных параметров
export type IGetEntitiesInput = Queries.Extension.GetEntities.IInput;
export type IGetEntityInput = Queries.Extension.GetEntity.IInput['data'];
// Типы выходных данных мутаций
export type ICreateEntityInput = Mutations.Extension.CreateEntity.IInput['data'];
export type ICreateEntityOutput =
Mutations.Extension.CreateEntity.IOutput[typeof Mutations.Extension.CreateEntity.name];
```
**Для сложных сущностей с отношениями:**
```typescript
// Базовый тип из Zeus
export type IEntity = Zeus.ModelTypes['ExtensionEntity'];
// Тип с отношениями
export type IEntityWithRelations =
Queries.Extension.GetEntityWithRelations.IOutput[typeof Queries.Extension.GetEntityWithRelations.name];
```
#### Правила именования типов
- `IEntityName` - основная сущность
- `IEntityNames` - массив сущностей
- `IEntityNamePagination` - пагинированный ответ
- `IGetEntityNameInput` - входные параметры для получения
- `ICreateEntityNameInput/Output` - вход/выход для создания
- `IUpdateEntityNameInput/Output` - вход/выход для обновления
- `IDeleteEntityNameInput/Output` - вход/выход для удаления
### Организация Store (model/store.ts)
#### Структура store
```typescript
import { defineStore } from 'pinia';
import { ref, Ref } from 'vue';
import { api } from '../api';
import type { IEntity, IEntitiesPagination, IGetEntityInput } from './types';
const namespace = 'entityStore';
interface IEntityStore {
// Реактивные состояния
entities: Ref<IEntitiesPagination | null>;
entity: Ref<IEntity | null>;
// Методы загрузки данных (только запросы!)
loadEntities: (data: IGetEntitiesInput) => Promise<void>;
loadEntity: (data: IGetEntityInput) => Promise<IEntity>;
}
export const useEntityStore = defineStore(namespace, (): IEntityStore => {
// Реактивные ref'ы
const entities = ref<IEntitiesPagination | null>(null);
const entity = ref<IEntity | null>(null);
// Методы загрузки (только чтение!)
const loadEntities = async (data: IGetEntitiesInput): Promise<void> => {
const loadedData = await api.loadEntities(data);
entities.value = loadedData;
};
const loadEntity = async (data: IGetEntityInput): Promise<IEntity> => {
const loadedData = await api.loadEntity(data);
entity.value = loadedData;
return loadedData;
};
return {
entities,
entity,
loadEntities,
loadEntity,
};
});
```
#### Правила для store
- **Только запросы**: Store содержит только методы чтения данных
- **Реактивное состояние**: Все данные хранятся в ref'ах
- **Интерфейс типизации**: Обязателен для типизации store
- **Namespace**: Уникальное имя для каждого store
- **Методы обновления списка**: Для поддержания консистентности данных
#### Методы управления списком
```typescript
const addEntityToList = (entityData: IEntity) => {
if (entities.value) {
const existingIndex = entities.value.items.findIndex(
(item) => item._id === entityData._id,
);
if (existingIndex !== -1) {
// Заменяем существующую
entities.value.items[existingIndex] = entityData;
} else {
// Добавляем новую в начало
entities.value.items = [entityData, ...entities.value.items];
entities.value.totalCount += 1;
}
}
};
const updateEntityInList = (entityData: IEntity) => {
if (entities.value) {
const existingIndex = entities.value.items.findIndex(
(item) => item._id === entityData._id,
);
if (existingIndex !== -1) {
// Обновляем существующую на месте
entities.value.items[existingIndex] = entityData;
}
}
};
const removeEntityFromList = (entityId: string) => {
if (entities.value) {
const index = entities.value.items.findIndex(
(item) => item._id === entityId,
);
if (index !== -1) {
entities.value.items.splice(index, 1);
entities.value.totalCount -= 1;
}
}
};
```
### Организация API (api/index.ts)
#### Структура API функций
```typescript
import { client } from 'src/shared/api/client';
import { Queries } from '@coopenomics/sdk';
import type { IEntity, IGetEntityInput } from '../model';
async function loadEntities(data: IGetEntitiesInput): Promise<IEntitiesPagination> {
const { [Queries.Extension.GetEntities.name]: output } = await client.Query(
Queries.Extension.GetEntities.query,
{
variables: data,
},
);
return output;
}
async function loadEntity(data: IGetEntityInput): Promise<IEntity> {
const { [Queries.Extension.GetEntity.name]: output } = await client.Query(
Queries.Extension.GetEntity.query,
{
variables: {
data,
},
},
);
return output;
}
export const api = {
loadEntities,
loadEntity,
};
```
#### Правила для API
- **Только функции-запросы**: Никаких мутаций в API сущностей
- **Строгая типизация**: Все параметры и возвраты типизированы
- **Использование SDK**: Все запросы через GraphQL Zeus SDK
- **Единый экспорт**: Все функции экспортируются через объект `api`
### Экспорты и индексные файлы
#### Сущность (entities/EntityName/index.ts)
```typescript
export * as EntityNameModel from './model';
export * from './api';
```
#### Слой сущностей (entities/index.ts)
```typescript
export * as EntityName from './EntityName';
export * as AnotherEntity from './AnotherEntity';
```
#### Расширение (extensions/extension-name/index.ts)
```typescript
export * from './entities';
export * from './features';
export * from './widgets';
```
### Важные принципы
#### Типизация
- Все типы из SDK GraphQL Zeus
- Строгая типизация всех входов/выходов
- Интерфейсы для store объектов
#### Реактивность
- Все данные в Pinia stores
- Использование Vue ref/computed
- Автоматические обновления UI
#### Запреты
- ❌ Мутации в сущностях
- ❌ Прямые запросы к API в компонентах
- ❌ Неиспользуемые переменные/импорты
@@ -1,130 +0,0 @@
---
globs: ["**/desktop/extensions/**/*"]
alwaysApply: false
---
## Расширения (Extensions)
### Общее устройство
Расширения - это изолированные модули приложения, каждый из которых реализует отдельный функционал. Расширения максимально независимы и следуют принципам Feature Sliced Design.
### Структура расширения
```
extension-name/
entities/ # Сущности расширения
features/ # Фичи расширения
widgets/ # Виджеты расширения
pages/ # Страницы расширения
index.ts # export * from './entities' etc.
install.ts # IWorkspaceConfig с маршрутами и меню
```
### Файл install.ts
#### Роль и назначение
Файл `install.ts` отвечает за интеграцию расширения в систему рабочих столов. Он:
- Определяет маршруты расширения
- Настраивает меню рабочего стола
- Управляет видимостью в левом дровере
- Задает роли доступа к разделам
#### Структура конфигурации
```typescript
import { markRaw } from 'vue';
import type { IWorkspaceConfig } from 'src/shared/lib/types/workspace';
export default async function (): Promise<IWorkspaceConfig> {
return {
workspace: 'extension-name', // Уникальный идентификатор рабочего стола
title: 'Название рабочего стола', // Отображаемое название
defaultRoute: 'default-route', // Маршрут по умолчанию
routes: [
{
meta: {
title: 'Название раздела',
icon: 'fa-solid fa-icon',
roles: ['chairman', 'member', 'user'], // Роли с доступом: chairman, member, user. Пустой массив [] - доступ у всех
},
path: '/:coopname/extension-name',
name: 'extension-name',
component: markRaw(MainComponent),
children: [
// Вложенные маршруты
],
},
],
};
}
```
#### Уровни видимости в меню
##### Первый уровень: Рабочий стол
- Расширение появляется в меню рабочих столов
- Задается в корневом маршруте с `path: '/:coopname/extension-name'`
##### Второй уровень: Левый дровер
- Кнопки разделов внутри рабочего стола
- Управляется свойством `hidden: true/false` в meta роута
- Если `hidden: true` - раздел не отображается в левом дровере
#### Пример с видимостью
```typescript
{
path: 'projects/:project_hash/tasks/:issue_hash',
name: 'project-issue',
component: markRaw(IssuePage),
meta: {
title: 'Задача',
icon: 'fa-solid fa-task',
roles: [],
agreements: agreementsBase,
requiresAuth: true,
hidden: true, // Не показывать в левом дровере
},
}
```
### Экспорты расширения
#### index.ts расширения
```typescript
export * from './entities';
export * from './features';
export * from './widgets';
export * from './pages';
```
#### Регистрация в системе
Расширения регистрируются через систему расширений и становятся доступны в магазине расширений для установки.
### Правила изоляции
#### Запреты
- ❌ Импорты между расширениями запрещены
- ❌ Совместное использование сущностей между расширениями
- ❌ Прямые зависимости между расширениями
#### Разрешения
- ✅ Импорт из `src/shared` (общие компоненты)
- ✅ Исключения: `FailAlert`, `SuccessAlert` (глобальные уведомления)
### Важные принципы
#### Разделение ответственности
- **Entities**: Только данные и их загрузка
- **Features**: Бизнес-логика и действия
- **Widgets**: Комплексные UI компоненты
- **Pages**: Корневые страницы расширения
#### Конфигурация меню
- Роли доступа: `chairman`, `member`, `user`
- Пустой массив `roles: []` означает доступ у всех ролей
- Видимость управляется через `hidden` свойство
- Иконки и названия должны быть информативными
#### Разработка расширений
- Расширения разрабатываются независимо
- Каждый расширение - отдельный функциональный модуль
- Возможность отключения/включения расширений
@@ -1,234 +0,0 @@
---
globs: ["**/desktop/features/**/*.ts", "**/desktop/features/**/*.vue"]
alwaysApply: false
---
## Архитектура фич (Features Layer)
### Общее устройство
Фичи - это бизнес-логика приложения, реализованная в виде Vue composables. Каждая фича отвечает за конкретное действие или набор связанных действий. Фичи взаимодействуют с entities для получения и обновления данных.
**Запрещено**: Размещать методы чтения данных в фичах. Чтение данных - задача entities.
### Структура папок фичи
```
features/
FeatureName/
index.ts # Экспорт composable и UI (если есть)
api/
index.ts # Мутации GraphQL через SDK
model/
index.ts # Экспорт composable функции
ui/ # Опционально: UI компоненты фичи
index.ts # Экспорт UI компонентов
Component.vue # Компоненты фичи
```
### Типизация фич
#### Импорты типов напрямую из SDK
```typescript
import { Mutations } from '@coopenomics/sdk';
// Типы входных данных мутации
export type IFeatureInput = Mutations.Extension.FeatureAction.IInput['data'];
// Типы выходных данных мутации
export type IFeatureOutput = Mutations.Extension.FeatureAction.IOutput[typeof Mutations.Extension.FeatureAction.name];
```
**Важно**: Ничего не переопределяем вручную! Все типы берутся напрямую из GraphQL Zeus SDK.
### Организация API (api/index.ts)
#### Структура API функций для мутаций
```typescript
import { client } from 'src/shared/api/client';
import { Mutations } from '@coopenomics/sdk';
import type { IFeatureInput, IFeatureOutput } from '../model';
async function featureAction(
data: IFeatureInput,
): Promise<IFeatureOutput> {
const { [Mutations.Extension.FeatureAction.name]: result } =
await client.Mutation(Mutations.Extension.FeatureAction.mutation, {
variables: {
data,
},
});
return result;
}
export const api = {
featureAction,
};
```
#### Правила для API фич
- **Только мутации**: API фич содержит только GraphQL мутации
- **Строгая типизация**: Все параметры и возвраты типизированы через SDK
- **Использование SDK**: Все мутации через GraphQL Zeus SDK
- **Единый экспорт**: Все функции экспортируются через объект `api`
### Организация Model (model/index.ts)
#### Структура composable функции
```typescript
import { useEntityStore } from 'app/extensions/extension-name/entities/Entity/model';
import { api } from '../api';
import type { IFeatureInput, IFeatureOutput } from '../api';
export function useFeatureAction() {
const store = useEntityStore();
const featureAction = async (input: IFeatureInput): Promise<IFeatureOutput> => {
const result = await api.featureAction(input);
if (result) {
// Обновляем store сущности для поддержания консистентности
store.updateEntityInList(result);
// или store.removeEntityFromList(input.entity_id);
// или store.addEntityToList(result);
}
return result;
};
return { featureAction };
}
```
#### Правила для composables
- **Использование store**: Фичи обновляют store entities после выполнения мутаций
- **Поддержание консистентности**: Данные в store всегда актуальны после действий
- **Возвращение результата**: Composable возвращает результат мутации
- **Обработка ошибок**: Ошибки обрабатываются в UI компонентах
### UI слой фич (опционально)
#### Когда использовать UI в фичах
- **Иногда, но не всегда**: UI слой используется только для специфических компонентов фичи
- **Простые действия**: Для кнопок, форм, специфичных для данной фичи
- **Не общие компоненты**: Не размещать shared компоненты в фичах
#### Экспорты UI компонентов
##### В ui/index.ts (первый уровень экспорта)
```typescript
export { default as FeatureButton } from './FeatureButton.vue';
export { default as FeatureDialog } from './FeatureDialog.vue';
```
##### В корне фичи index.ts (второй уровень экспорта)
```typescript
export { useFeatureAction } from './model';
export * from './ui'; // Экспортируем все UI компоненты
```
### Экспорты и индексные файлы
#### Фича (features/FeatureName/index.ts)
```typescript
export { useFeatureAction } from './model';
export * from './ui'; // Если есть UI слой
```
#### Слой фич (features/index.ts)
```typescript
export { useFeatureAction } from './FeatureName';
export { useAnotherFeature } from './AnotherFeature';
```
#### Расширение (extensions/extension-name/index.ts)
```typescript
export * from './entities';
export * from './features'; // Экспорт всех фич расширения
export * from './widgets';
export * from './pages';
```
### Принципы работы с фичами
#### Разделение ответственности
- **Entities**: Хранение и чтение данных
- **Features**: Бизнес-логика и действия (мутации)
- **Widgets**: Сложные UI компоненты
- **Pages**: Корневые страницы
#### Взаимодействие со stores
- **Обязательное обновление**: После каждой мутации обновлять соответствующий store
- **Консистентность данных**: UI всегда показывает актуальные данные из store
- **Оптимистичные обновления**: Возможны для лучшего UX
#### Типизация
- **SDK-first**: Все типы из GraphQL Zeus SDK
- **Не переопределять**: Никаких ручных переопределений типов
- **Строгая типизация**: Все входы/выходы типизированы
#### Организация кода
- **Composable pattern**: Каждая фича - отдельный composable
- **Единая ответственность**: Одна фича = одно действие или группа связанных действий
- **Переиспользование**: Фичи могут использоваться в разных частях приложения
### Примеры
#### Простая фича (только composable)
```
DeleteStory/
api/
index.ts # Мутация deleteStory
model/
index.ts # useDeleteStory composable
index.ts # Экспорт composable
```
#### Фича с UI компонентами
```
CreateProject/
api/
index.ts # Мутация createProject
model/
index.ts # useCreateProject composable
ui/
index.ts # Экспорт CreateProjectButton
CreateProjectButton.vue
index.ts # Экспорт composable + UI
```
#### Использование фичи в компоненте
```vue
<script setup>
import { useDeleteStory } from 'features/Story/DeleteStory';
const { deleteStory } = useDeleteStory();
// Использование в обработчике
const handleDelete = async () => {
try {
await deleteStory({ story_hash: '...' });
// Обработка успеха
} catch (error) {
// Обработка ошибки
}
};
</script>
```
### Важные принципы
#### Четкое разделение слоев
- **Entities** управляют данными
- **Features** управляют действиями
- **UI** управляет интерфейсом
#### Консистентность архитектуры
- Все фичи следуют единому паттерну
- Типизация через SDK
- Обновление stores после мутаций
#### Производительность
- Оптимистичные обновления где возможно
- Минимальные перерисовки UI
- Эффективное управление состоянием
-291
View File
@@ -1,291 +0,0 @@
---
globs: ["**/desktop/pages/**/*.ts", "**/desktop/pages/**/*.vue"]
alwaysApply: false
---
## Архитектура страниц (Pages Layer)
### Общее устройство
Страницы - это корневые компоненты приложения, которые используются в маршрутах. Страницы отвечают за оркестрацию виджетов, управление состоянием UI и навигацию. Они могут быть как простыми контейнерами, так и сложными композитными компонентами.
### Структура папок страницы
```
pages/
PageName/
index.ts # Экспорт страницы
ui/ # Vue компонент страницы
PageName.vue # Основной компонент
index.ts # Экспорт из ui
```
### Типы страниц
#### 1. Контейнерные страницы
Простые страницы, которые делегируют отображение дочерним маршрутам:
```vue
<template lang="pug">
div
// Лоадер пока идет предзагрузка данных
WindowLoader(v-if="isLoading", text="Загрузка данных...")
// Основной контент после загрузки
router-view(v-else)
</template>
<script setup>
import { ref, onMounted } from 'vue';
import { WindowLoader } from 'src/shared/ui/Loader';
import { useContributorStore } from 'entities/Contributor/model';
const contributorStore = useContributorStore();
const isLoading = ref(true);
onMounted(async () => {
// Предзагрузка необходимых данных
await contributorStore.loadSelf({ username: session.username });
isLoading.value = false;
});
</script>
```
#### 2. Композитные страницы
Страницы с сложной композицией виджетов и собственной логикой:
```vue
<template lang="pug">
div
q-card(flat)
// Композиция виджетов
StoriesWidget(
:stories='storyStore.stories?.items || []',
:loading='loading'
)
// Собственная логика пагинации
q-card-actions(align='center')
q-pagination(
v-model='pagination.page',
:max='Math.ceil((storyStore.stories?.totalCount || 0) / pagination.rowsPerPage)',
@update:model-value='onPageChange'
)
</template>
<script setup>
import { ref, onMounted } from 'vue';
import { StoriesWidget } from 'widgets/StoriesWidget';
import { useStoryStore } from 'entities/Story/model';
const storyStore = useStoryStore();
const loading = ref(false);
// Собственная логика пагинации
const pagination = ref({
page: 1,
rowsPerPage: 25,
});
const loadData = async () => {
loading.value = true;
try {
await storyStore.loadStories({
filter: { /* фильтры */ },
pagination: pagination.value,
});
} catch (error) {
FailAlert(`Ошибка загрузки: ${error.message}`);
} finally {
loading.value = false;
}
};
</script>
```
### Принципы организации страниц
#### Использование слоев
```typescript
// ✅ Правильно: страницы используют все нижележащие слои
import { useEntityStore } from 'entities/Entity/model'; // Данные
import { useEntityAction } from 'features/Entity/EntityAction'; // Действия
import { EntityWidget } from 'widgets/EntityWidget'; // Композиция UI
// ❌ Неправильно: страницы не используют другие страницы
// import { OtherPage } from 'pages/OtherPage';
```
#### Управление состоянием
Страницы управляют:
- **Загрузкой данных**: инициализация, фильтры, пагинация
- **UI состоянием**: загрузка, ошибки, навигация
- **Взаимодействием**: обработка событий от виджетов
#### Композиция виджетов
```vue
<template lang="pug">
// Простая композиция
EntityWidget(:items='store.items')
// Сложная композиция со слотами
LayoutWidget
template(#header)
PageHeader(title='Заголовок страницы')
template(#content)
EntityWidget(:items='store.items')
template(#sidebar)
FiltersWidget(@filter-changed='onFilterChange')
</template>
```
### Экспорты и индексные файлы
#### Страница (pages/PageName/index.ts)
```typescript
export { default as PageName } from './ui/PageName.vue';
```
#### Слой страниц (pages/index.ts)
```typescript
export * from './PageName';
export * from './AnotherPage';
```
#### Использование в маршрутах
Страницы используются в `install.ts` расширения:
```typescript
routes: [{
path: '/:coopname/extension/page',
name: 'page',
component: markRaw(PageName), // Импорт из pages
meta: { /* ... */ }
}]
```
### Принципы работы со страницами
#### Разделение ответственности
- **Entities**: Данные и их хранение
- **Features**: Бизнес-логика и действия
- **Widgets**: Композиция UI компонентов
- **Pages**: Оркестрация всего вышеперечисленного
#### Загрузка данных
Страницы отвечают за:
- Инициализацию данных при монтировании
- Управление пагинацией и фильтрами
- Обработку ошибок загрузки
- Кеширование и оптимизацию запросов
#### Навигация и взаимодействие
```typescript
const router = useRouter();
const onItemClick = (item) => {
router.push({
name: 'item-details',
params: { id: item.id }
});
};
const onActionComplete = () => {
// Обновление данных после действия
loadData();
};
```
### Примеры использования
#### Простая страница списка
```vue
<template lang="pug">
div
q-card(flat)
// Виджет таблицы
ItemsListWidget(
:items='store.items',
:loading='loading',
@item-click='onItemClick'
)
// Пагинация
q-pagination(
v-model='currentPage',
:max='totalPages',
@update:model-value='loadPage'
)
</template>
<script setup>
import { ref, computed } from 'vue';
import { ItemsListWidget } from 'widgets/ItemsListWidget';
import { useItemStore } from 'entities/Item/model';
const store = useItemStore();
const currentPage = ref(1);
const totalPages = computed(() =>
Math.ceil((store.items?.totalCount || 0) / 25)
);
const loadPage = async (page) => {
await store.loadItems({ page });
};
</script>
```
#### Страница с несколькими виджетами
```vue
<template lang="pug">
.q-pa-md
// Заголовок страницы
PageHeader(title='Дашборд')
.row.q-gutter-md
.col-12.col-md-6
// Статистика
StatsWidget(:stats='stats')
.col-12.col-md-6
// Недавние действия
RecentActionsWidget(:actions='recentActions')
// Основной контент
MainContentWidget(
:data='mainData',
@refresh='loadData'
)
</template>
<script setup>
import { onMounted } from 'vue';
import PageHeader from 'src/shared/ui/PageHeader';
import StatsWidget from 'widgets/StatsWidget';
import RecentActionsWidget from 'widgets/RecentActionsWidget';
import MainContentWidget from 'widgets/MainContentWidget';
onMounted(() => {
loadData();
});
</script>
```
### Важные принципы
#### Производительность
- Ленивая загрузка данных
- Оптимизация перерисовок
- Эффективное управление памятью
#### Доступность
- Правильная структура заголовков
- Клавиатурная навигация
- Screen reader поддержка
#### SEO и метаданные
- Управление title страницы
- Meta теги для социальных сетей
- Структурированные данные
@@ -1,388 +0,0 @@
---
globs: ["**/desktop/widgets/**/*.ts", "**/desktop/widgets/**/*.vue"]
alwaysApply: false
---
## Архитектура виджетов (Widgets Layer)
### Общее устройство
Виджеты - это комплексные UI компоненты, которые комбинируют данные из entities и действия из features для создания полноценных интерфейсных блоков. Виджеты отвечают за композицию и представление сложных частей интерфейса.
**Запрещено**: Виджеты не переиспользуют другие виджеты. Виджеты могут использовать только фичи и shared компоненты.
### Структура папок виджета
```
widgets/
WidgetName/
index.ts # Экспорт виджета
WidgetName.vue # Vue компонент виджета
# или
ui/
WidgetName.vue # Vue компонент в подпапке
index.ts # Экспорт из ui
```
### Принципы организации виджетов
#### Использование только разрешенных слоев
```typescript
// ✅ Разрешено: entities для данных
import { useEntityStore, type IEntity } from 'entities/Entity/model';
// ✅ Разрешено: features для действий
import { useEntityAction } from 'features/Entity/EntityAction';
import { EntityActionButton } from 'features/Entity/EntityAction';
// ✅ Разрешено: shared компоненты
import { SharedComponent } from 'src/shared/ui/SharedComponent';
// ❌ Запрещено: импорт других виджетов
// import { AnotherWidget } from 'widgets/AnotherWidget';
```
#### Композиция через слоты (опционально)
Виджеты могут содержать слоты для композиции с другими компонентами на уровне страниц:
```vue
<template lang="pug">
.widget-container
.widget-header
slot(name='header')
| Заголовок по умолчанию
.widget-content
slot(name='content')
| Контент по умолчанию
.widget-actions
slot(name='actions')
</template>
```
### Типы виджетов
#### 1. Презентационные виджеты
Отображают данные в виде таблиц, списков, карточек:
```vue
<template lang="pug">
q-card(flat)
q-table(
:rows='entities',
:columns='columns',
:loading='loading'
)
</template>
<script setup>
import { useEntityStore } from 'entities/Entity/model';
const store = useEntityStore();
// Логика загрузки и отображения данных
</script>
```
#### 2. Интерактивные виджеты
Комбинируют отображение данных с действиями:
```vue
<template lang="pug">
.widget
// Отображение данных
.data-section
EntityItem(v-for='item in items', :item='item')
// Действия
.actions-section
CreateEntityButton
UpdateEntityButton
</template>
```
#### 3. Композитные виджеты со слотами
Предоставляют гибкую структуру для страниц:
```vue
<template lang="pug">
.layout-widget
header
slot(name='header')
main
slot(name='main')
aside
slot(name='sidebar')
footer
slot(name='footer')
</template>
```
### Props и конфигурация
#### Гибкая конфигурация через props
```typescript
interface Props {
// Данные
items?: IEntity[];
loading?: boolean;
// Конфигурация отображения
showActions?: boolean;
maxItems?: number;
emptyMessage?: string;
// Callbacks для взаимодействия
onItemClick?: (item: IEntity) => void;
onActionComplete?: () => void;
}
const props = withDefaults(defineProps<Props>(), {
showActions: true,
maxItems: 50,
emptyMessage: 'Нет данных',
});
```
#### Управление состоянием
```typescript
// Локальное состояние виджета
const expandedItems = ref<Set<string>>(new Set());
const selectedItem = ref<IEntity | null>(null);
// Props для внешнего управления
interface Props {
expandedItems?: string[];
selectedItemId?: string;
}
```
### Использование entities в виджетах
#### Чтение данных из stores
```typescript
import { useEntityStore } from 'entities/Entity/model';
const store = useEntityStore();
// Доступ к реактивным данным (уже реактивны через Pinia)
const entities = computed(() => store.entities);
const loading = computed(() => store.loading);
// Методы загрузки
const loadData = () => {
store.loadEntities({ filter: props.filter });
};
```
#### Важно о реактивности stores
**Store автоматически реактивен через Pinia** - не нужно использовать `storeToRefs()` или `.value`:
```typescript
const store = useEntityStore();
// ✅ Правильно - данные уже реактивны
const entities = computed(() => store.entities);
// ❌ Неправильно - storeToRefs не нужен
// const { entities } = storeToRefs(store);
// ❌ Неправильно - данные уже реактивны, .value не нужен
// const entities = store.entities.value;
```
### Использование features в виджетах
#### Импорт и использование фич
```typescript
import { useEntityAction } from 'features/Entity/EntityAction';
import { EntityActionButton } from 'features/Entity/EntityAction';
const { entityAction } = useEntityAction();
// Использование в обработчиках
const handleAction = async () => {
try {
await entityAction(params);
// Обработка успеха
} catch (error) {
// Обработка ошибки
}
};
```
#### Интеграция UI компонентов фич
```vue
<template>
<div class="widget">
<!-- Данные -->
<EntityList :items="entities" />
<!-- Действия из фич -->
<div class="actions">
<CreateEntityButton @created="onEntityCreated" />
<UpdateEntityButton :entity="selectedEntity" />
</div>
</div>
</template>
```
### Обработка событий и коммуникация
#### Внутренняя коммуникация
```typescript
const emit = defineEmits<{
itemSelected: [item: IEntity];
actionCompleted: [action: string, result: any];
}>();
// Генерация событий
const onItemClick = (item: IEntity) => {
emit('itemSelected', item);
};
const onActionSuccess = (result: any) => {
emit('actionCompleted', 'create', result);
};
```
#### Внешнее управление через props
```typescript
interface Props {
selectedItemId?: string;
onItemSelect?: (item: IEntity) => void;
}
const props = defineProps<Props>();
// Синхронизация с внешним состоянием
watch(() => props.selectedItemId, (newId) => {
if (newId) {
const item = entities.value.find(e => e.id === newId);
if (item) {
selectedItem.value = item;
props.onItemSelect?.(item);
}
}
});
```
### Экспорты и индексные файлы
#### Виджет (widgets/WidgetName/index.ts)
```typescript
export { default as WidgetName } from './WidgetName.vue';
// или
export { default as WidgetName } from './ui/WidgetName.vue';
```
#### Слой виджетов (widgets/index.ts)
```typescript
export * from './WidgetName';
export * from './AnotherWidget';
```
#### Расширение (extensions/extension-name/index.ts)
```typescript
export * from './entities';
export * from './features';
export * from './widgets'; // Экспорт всех виджетов расширения
export * from './pages';
```
### Примеры использования
#### Простой виджет списка
```vue
<template lang="pug">
q-card(flat)
q-card-section
.text-h6 Список элементов
q-card-section
q-list
q-item(
v-for='item in items',
:key='item.id',
clickable,
@click='$emit("itemClick", item)'
)
q-item-section {{ item.name }}
.text-center.q-pa-md(v-if='!items.length && !loading')
| {{ emptyMessage }}
.text-center.q-pa-md(v-if='loading')
q-spinner(color='primary')
</template>
<script setup lang="ts">
import type { IEntity } from 'entities/Entity/model';
interface Props {
items: IEntity[];
loading?: boolean;
emptyMessage?: string;
}
defineEmits<{
itemClick: [item: IEntity];
}>();
</script>
```
#### Виджет с действиями
```vue
<template lang="pug">
.widget
.widget-header
h3 {{ title }}
CreateEntityButton(@created='onEntityCreated')
.widget-content
EntityTable(
:entities='entities',
:loading='loading',
@entity-selected='onEntitySelected'
)
.widget-footer
q-pagination(
v-model='currentPage',
:max='totalPages',
@update:model-value='onPageChange'
)
</template>
<script setup lang="ts">
import { useEntityStore } from 'entities/Entity/model';
import { CreateEntityButton } from 'features/Entity/CreateEntity';
import EntityTable from './EntityTable.vue'; // Shared компонент
const store = useEntityStore();
const { entities, loading } = storeToRefs(store);
// Логика пагинации, фильтрации и т.д.
</script>
```
### Важные принципы
#### Разделение ответственности
- **Entities**: Данные и их хранение
- **Features**: Бизнес-логика и действия
- **Widgets**: Композиция UI компонентов
- **Pages**: Оркестрация виджетов
#### Композиция через слоты
- Виджеты могут предоставлять слоты для гибкой композиции
- Страницы используют слоты для вставки виджетов
- Избегать жесткой связи между виджетами
#### Переиспользование
- Виджеты могут переиспользоваться в разных страницах
- Конфигурация через props для адаптации
- Shared компоненты для общих элементов UI
#### Производительность
- Ленивая загрузка данных
- Виртуализация для больших списков
- Оптимистичные обновления через features
@@ -21,7 +21,7 @@ interface IContributorStore {
loadSelf: (data: IGetContributorInput) => Promise<IContributor | null | undefined>;
updateSelf: (contributorData: IContributor) => void;
hasClearance: (projectHash: string) => boolean;
isGenerationAgreementCompleted: ComputedRef<boolean>;
isGenerationContractCompleted: ComputedRef<boolean>;
isCapitalAgreementCompleted: ComputedRef<boolean>;
}
@@ -70,7 +70,7 @@ export const useContributorStore = defineStore(
};
// Вычисляемые свойства для проверки завершения шагов регистрации
const isGenerationAgreementCompleted = computed(() => {
const isGenerationContractCompleted = computed(() => {
return !!self.value?.contract;
});
@@ -87,7 +87,7 @@ export const useContributorStore = defineStore(
loadSelf,
updateSelf,
hasClearance,
isGenerationAgreementCompleted,
isGenerationContractCompleted,
isCapitalAgreementCompleted,
};
},
@@ -7,15 +7,15 @@ export type IMakeClearanceInput =
export type IMakeClearanceOutput =
Mutations.Capital.MakeClearance.IOutput[typeof Mutations.Capital.MakeClearance.name];
export type IGenerateProjectGenerationAgreementInput =
Mutations.Capital.GenerateProjectGenerationAgreement.IInput['data'];
export type IGenerateProjectGenerationAgreementOutput =
Mutations.Capital.GenerateProjectGenerationAgreement.IOutput[typeof Mutations.Capital.GenerateProjectGenerationAgreement.name];
export type IGenerateProjectGenerationContractInput =
Mutations.Capital.GenerateProjectGenerationContract.IInput['data'];
export type IGenerateProjectGenerationContractOutput =
Mutations.Capital.GenerateProjectGenerationContract.IOutput[typeof Mutations.Capital.GenerateProjectGenerationContract.name];
export type IGenerateComponentGenerationAgreementInput =
Mutations.Capital.GenerateComponentGenerationAgreement.IInput['data'];
export type IGenerateComponentGenerationAgreementOutput =
Mutations.Capital.GenerateComponentGenerationAgreement.IOutput[typeof Mutations.Capital.GenerateComponentGenerationAgreement.name];
export type IGenerateComponentGenerationContractInput =
Mutations.Capital.GenerateComponentGenerationContract.IInput['data'];
export type IGenerateComponentGenerationContractOutput =
Mutations.Capital.GenerateComponentGenerationContract.IOutput[typeof Mutations.Capital.GenerateComponentGenerationContract.name];
export type { IGenerateDocumentInput, IGeneratedDocumentOutput };
@@ -32,12 +32,12 @@ async function makeClearance(
return result;
}
async function generateProjectGenerationAgreement(
data: IGenerateProjectGenerationAgreementInput,
options?: Mutations.Capital.GenerateProjectGenerationAgreement.IInput['options'],
): Promise<IGenerateProjectGenerationAgreementOutput> {
const { [Mutations.Capital.GenerateProjectGenerationAgreement.name]: result } =
await client.Mutation(Mutations.Capital.GenerateProjectGenerationAgreement.mutation, {
async function generateProjectGenerationContract(
data: IGenerateProjectGenerationContractInput,
options?: Mutations.Capital.GenerateProjectGenerationContract.IInput['options'],
): Promise<IGenerateProjectGenerationContractOutput> {
const { [Mutations.Capital.GenerateProjectGenerationContract.name]: result } =
await client.Mutation(Mutations.Capital.GenerateProjectGenerationContract.mutation, {
variables: {
data,
options,
@@ -47,12 +47,12 @@ async function generateProjectGenerationAgreement(
return result;
}
async function generateComponentGenerationAgreement(
data: IGenerateComponentGenerationAgreementInput,
options?: Mutations.Capital.GenerateComponentGenerationAgreement.IInput['options'],
): Promise<IGenerateComponentGenerationAgreementOutput> {
const { [Mutations.Capital.GenerateComponentGenerationAgreement.name]: result } =
await client.Mutation(Mutations.Capital.GenerateComponentGenerationAgreement.mutation, {
async function generateComponentGenerationContract(
data: IGenerateComponentGenerationContractInput,
options?: Mutations.Capital.GenerateComponentGenerationContract.IInput['options'],
): Promise<IGenerateComponentGenerationContractOutput> {
const { [Mutations.Capital.GenerateComponentGenerationContract.name]: result } =
await client.Mutation(Mutations.Capital.GenerateComponentGenerationContract.mutation, {
variables: {
data,
options,
@@ -64,6 +64,6 @@ async function generateComponentGenerationAgreement(
export const api = {
makeClearance,
generateProjectGenerationAgreement,
generateComponentGenerationAgreement,
generateProjectGenerationContract,
generateComponentGenerationContract,
};
@@ -3,10 +3,10 @@ import {
api,
type IMakeClearanceInput,
type IMakeClearanceOutput,
type IGenerateProjectGenerationAgreementInput,
type IGenerateProjectGenerationAgreementOutput,
type IGenerateComponentGenerationAgreementInput,
type IGenerateComponentGenerationAgreementOutput
type IGenerateProjectGenerationContractInput,
type IGenerateProjectGenerationContractOutput,
type IGenerateComponentGenerationContractInput,
type IGenerateComponentGenerationContractOutput
} from '../api';
import { useSignDocument } from 'src/shared/lib/document/model/entity';
import { useSessionStore } from 'src/entities/Session/model';
@@ -15,8 +15,8 @@ import type { IGenerateDocumentInput, IGeneratedDocumentOutput } from 'src/share
import type { IGetProjectOutput } from 'app/extensions/capital/entities/Project/model';
export type { IMakeClearanceInput, IMakeClearanceOutput };
export type { IGenerateProjectGenerationAgreementInput, IGenerateProjectGenerationAgreementOutput };
export type { IGenerateComponentGenerationAgreementInput, IGenerateComponentGenerationAgreementOutput };
export type { IGenerateProjectGenerationContractInput, IGenerateProjectGenerationContractOutput };
export type { IGenerateComponentGenerationContractInput, IGenerateComponentGenerationContractOutput };
export type { IGenerateDocumentInput, IGeneratedDocumentOutput };
export function useMakeClearance() {
@@ -37,26 +37,26 @@ export function useMakeClearance() {
}
};
const generateProjectGenerationAgreement = async (
input: IGenerateProjectGenerationAgreementInput,
options?: Parameters<typeof api.generateProjectGenerationAgreement>[1]
): Promise<IGenerateProjectGenerationAgreementOutput> => {
const generateProjectGenerationContract = async (
input: IGenerateProjectGenerationContractInput,
options?: Parameters<typeof api.generateProjectGenerationContract>[1]
): Promise<IGenerateProjectGenerationContractOutput> => {
isLoading.value = true;
try {
const result = await api.generateProjectGenerationAgreement(input, options);
const result = await api.generateProjectGenerationContract(input, options);
return result;
} finally {
isLoading.value = false;
}
};
const generateComponentGenerationAgreement = async (
input: IGenerateComponentGenerationAgreementInput,
options?: Parameters<typeof api.generateComponentGenerationAgreement>[1]
): Promise<IGenerateComponentGenerationAgreementOutput> => {
const generateComponentGenerationContract = async (
input: IGenerateComponentGenerationContractInput,
options?: Parameters<typeof api.generateComponentGenerationContract>[1]
): Promise<IGenerateComponentGenerationContractOutput> => {
isLoading.value = true;
try {
const result = await api.generateComponentGenerationAgreement(input, options);
const result = await api.generateComponentGenerationContract(input, options);
return result;
} finally {
isLoading.value = false;
@@ -99,7 +99,7 @@ export function useMakeClearance() {
throw new Error('Родительский проект не найден для компонента');
}
const generateInput: IGenerateComponentGenerationAgreementInput = {
const generateInput: IGenerateComponentGenerationContractInput = {
coopname,
username,
component_hash: project.project_hash,
@@ -107,17 +107,17 @@ export function useMakeClearance() {
lang: 'ru',
};
generatedDocument = await generateComponentGenerationAgreement(generateInput);
generatedDocument = await generateComponentGenerationContract(generateInput);
} else {
// Генерируем документ приложения к договору для проекта (1002)
const generateInput: IGenerateProjectGenerationAgreementInput = {
const generateInput: IGenerateProjectGenerationContractInput = {
coopname,
username,
project_hash: project.project_hash,
lang: 'ru',
};
generatedDocument = await generateProjectGenerationAgreement(generateInput);
generatedDocument = await generateProjectGenerationContract(generateInput);
}
// 2. Подписываем сгенерированный документ одинарной подписью
@@ -139,8 +139,8 @@ export function useMakeClearance() {
return {
makeClearance,
generateProjectGenerationAgreement,
generateComponentGenerationAgreement,
generateProjectGenerationContract,
generateComponentGenerationContract,
signGeneratedDocument,
respondToInvite,
isLoading,
@@ -1,5 +1,5 @@
import type { IRegisterContributorOutput } from 'app/extensions/capital/entities/Contributor/model';
import type { IRegisterContributorInput, IGenerateGenerationAgreementInput } from '../model';
import type { IRegisterContributorInput, IGenerateGenerationContractInput } from '../model';
import type {
IGenerateDocumentOptionsInput,
IGeneratedDocumentOutput,
@@ -20,12 +20,12 @@ async function registerContributor(
return result;
}
async function generateGenerationAgreement(
data: IGenerateGenerationAgreementInput,
async function generateGenerationContract(
data: IGenerateGenerationContractInput,
options?: IGenerateDocumentOptionsInput,
): Promise<IGeneratedDocumentOutput> {
const { [Mutations.Capital.GenerateGenerationAgreement.name]: result } =
await client.Mutation(Mutations.Capital.GenerateGenerationAgreement.mutation, {
const { [Mutations.Capital.GenerateGenerationContract.name]: result } =
await client.Mutation(Mutations.Capital.GenerateGenerationContract.mutation, {
variables: {
data,
options,
@@ -37,5 +37,5 @@ async function generateGenerationAgreement(
export const api = {
registerContributor,
generateGenerationAgreement,
generateGenerationContract,
};
@@ -14,8 +14,8 @@ import { generateUniqueHash } from 'src/shared/lib/utils/generateUniqueHash';
export type IRegisterContributorInput =
Mutations.Capital.RegisterContributor.IInput['data'];
export type IGenerateGenerationAgreementInput =
Mutations.Capital.GenerateGenerationAgreement.IInput['data'];
export type IGenerateGenerationContractInput =
Mutations.Capital.GenerateGenerationContract.IInput['data'];
export function useRegisterContributor() {
const store = useContributorStore();
@@ -54,13 +54,13 @@ export function useRegisterContributor() {
contributorHash.value = await generateUniqueHash();
}
const data: IGenerateGenerationAgreementInput = {
const data: IGenerateGenerationContractInput = {
coopname: system.info.coopname,
username: session.username,
contributor_hash: contributorHash.value,
};
console.log('🔐 Генерируем данные для генерации договора:', data);
generatedDocument.value = await api.generateGenerationAgreement(data);
generatedDocument.value = await api.generateGenerationContract(data);
return generatedDocument.value;
} catch (error) {
console.error('Ошибка при генерации договора:', error);
@@ -26,7 +26,7 @@ export const useCapitalOnboarding = () => {
// Маппинг шагов на registry_id
const stepToRegistryId: Record<string, number> = {
'generator_program_template': 994,
'generation_agreement_template': 997,
'generation_contract_template': 997,
'blagorost_program': 998,
'blagorost_offer_template': 999,
};
@@ -89,15 +89,15 @@ export const useCapitalOnboarding = () => {
hash: typeof state?.onboarding_generator_program_template_hash === 'string' && state.onboarding_generator_program_template_hash ? state.onboarding_generator_program_template_hash : null,
},
{
id: 'generation_agreement_template',
id: 'generation_contract_template',
title: 'Шаблон договора участия в хозяйственной деятельности',
description: 'Утверждение шаблона договора участия в хозяйственной деятельности для работы по программе',
question: 'О утверждении шаблона договора участия в хозяйственной деятельности',
decision: '', // Будет заполнено через генерацию документа
decisionPrefix: 'Утвердить шаблон договора участия в хозяйственной деятельности:',
status: state?.generation_agreement_template_done ? 'completed' :
state?.onboarding_generation_agreement_template_hash ? 'in_progress' : 'pending',
hash: typeof state?.onboarding_generation_agreement_template_hash === 'string' && state.onboarding_generation_agreement_template_hash ? state.onboarding_generation_agreement_template_hash : null,
status: state?.generation_contract_template_done ? 'completed' :
state?.onboarding_generation_contract_template_hash ? 'in_progress' : 'pending',
hash: typeof state?.onboarding_generation_contract_template_hash === 'string' && state.onboarding_generation_contract_template_hash ? state.onboarding_generation_contract_template_hash : null,
},
{
id: 'blagorost_program',
@@ -109,7 +109,7 @@ export const useCapitalOnboarding = () => {
status: state?.blagorost_provision_done ? 'completed' :
state?.onboarding_blagorost_provision_hash ? 'in_progress' : 'pending',
hash: typeof state?.onboarding_blagorost_provision_hash === 'string' && state.onboarding_blagorost_provision_hash ? state.onboarding_blagorost_provision_hash : null,
depends_on: ['generation_agreement_template'], // Зависит от утверждения шаблона договора
// depends_on: ['generation_contract_template'], // Зависит от утверждения шаблона договора
},
{
id: 'blagorost_offer_template',
@@ -40,7 +40,7 @@ const { isOnboardingCompleted, loadState } = useCapitalOnboarding();
// Проверка полной регистрации (есть контракт И есть соглашение с программой)
const isFullyRegistered = computed(() => {
return contributorStore.isGenerationAgreementCompleted && contributorStore.isCapitalAgreementCompleted;
return contributorStore.isGenerationContractCompleted && contributorStore.isCapitalAgreementCompleted;
});
// Проверка завершения онбординга капитализации
@@ -440,7 +440,7 @@ const prevStep = () => {
// Обновление текущего шага на основе состояния регистрации
const updateCurrentStep = () => {
if (!contributorStore.isGenerationAgreementCompleted) {
if (!contributorStore.isGenerationContractCompleted) {
// Если регистрация участия не завершена, начинаем с выбора ролей
currentStep.value = steps.roles;
} else if (!contributorStore.isCapitalAgreementCompleted) {
@@ -453,7 +453,7 @@ const updateCurrentStep = () => {
};
// Следим только за изменениями статуса регистрации
watch(() => contributorStore.isGenerationAgreementCompleted, updateCurrentStep);
watch(() => contributorStore.isGenerationContractCompleted, updateCurrentStep);
watch(() => contributorStore.isCapitalAgreementCompleted, updateCurrentStep);
/**
@@ -119,7 +119,6 @@ export const useRegistratorStore = defineStore(
email: '',
selectedBranch: '',
selectedProgramKey: '',
requiresProgramSelection: false,
account: structuredClone(initialAccountState),
userData: structuredClone(initialUserDataState),
signature: '',
@@ -162,10 +161,18 @@ export const useRegistratorStore = defineStore(
() => system.info?.cooperator_account.is_branched,
);
// Проверяем необходимость выбора программы на основе типа аккаунта
const requiresProgramSelection = computed(() => {
const accountType = state.userData.type;
// Для voskhod показываем выбор программы для индивидуальных и предпринимательских аккаунтов
return system.info?.coopname === 'voskhod' &&
(accountType === 'individual' || accountType === 'entrepreneur');
});
const filteredSteps = computed(() =>
stepNames.filter((step) => {
if (step === 'SelectBranch' && !isBranched.value) return false;
if (step === 'SelectProgram' && !state.requiresProgramSelection) return false;
if (step === 'SelectProgram' && !requiresProgramSelection.value) return false;
return true;
}),
);
@@ -215,7 +222,6 @@ export const useRegistratorStore = defineStore(
state.step = 1;
state.selectedBranch = '';
state.selectedProgramKey = '';
state.requiresProgramSelection = false;
state.email = '';
state.account = structuredClone(initialAccountState);
state.agreements = structuredClone(initialAgreementsState);
@@ -140,7 +140,7 @@ export function useCreateUser() {
privacy_agreement: privacyDoc?.signed_document || store.privacyAgreement,
signature_agreement: signatureDoc?.signed_document || store.signatureAgreement,
user_agreement: userDoc?.signed_document || store.userAgreement,
program_key: registratorStore.state.selectedProgramKey || undefined,
program_key: registratorStore.state.selectedProgramKey as Zeus.ProgramKey | undefined,
};
// Добавляем blagorost_offer если есть
@@ -18,7 +18,7 @@ q-step(
</template>
<script lang="ts" setup>
import { computed, watch, onMounted } from 'vue';
import { computed, watch, ref } from 'vue';
import { useCreateUser } from 'src/features/User/CreateUser';
import { FailAlert } from 'src/shared/api';
import { useSystemStore } from 'src/entities/System/model';
@@ -33,21 +33,37 @@ const api = useCreateUser();
const step = computed(() => store.state.step);
const coop = useCooperativeStore();
const isCreatingPayment = ref(false);
// Computed property для payment с правильной типизацией
const payment = computed(() => store.state.payment);
const currentStep = store.steps.PayInitial;
onMounted(async () => {
await coop.loadPublicCooperativeData(info.coopname);
// Загружаем данные кооператива один раз при инициализации
coop.loadPublicCooperativeData(info.coopname);
if (step.value === currentStep) createInitialPayment();
});
const createInitialPayment = async () => {
// Защита от повторных вызовов
if (isCreatingPayment.value || store.state.is_paid) return;
try {
isCreatingPayment.value = true;
await api.createInitialPayment();
} catch (e: any) {
FailAlert(
'Возникла ошибка на этапе оплаты. Попробуйте позже или обратитесь в поддержку.',
);
console.error(e);
} finally {
isCreatingPayment.value = false;
}
};
// Используем только watch с immediate: true вместо onMounted + watch
watch(step, (newValue) => {
if (newValue === currentStep) createInitialPayment();
});
}, { immediate: true });
watch(
() => store.state.is_paid,
@@ -59,19 +75,6 @@ watch(
},
);
const createInitialPayment = async () => {
try {
if (!store.state.is_paid) {
await api.createInitialPayment();
}
} catch (e: any) {
FailAlert(
'Возникла ошибка на этапе оплаты. Попробуйте позже или обратитесь в поддержку.',
);
console.error(e);
}
};
const paymentFail = (): void => {
FailAlert(
'Возникла ошибка на этапе оплаты. Попробуйте позже или обратитесь в поддержку.',
@@ -44,7 +44,7 @@ div
q-btn.q-mt-lg.q-mb-lg(color='primary', label='Продолжить', :disabled='!agreeWithAll' @click='registratorStore.next()')
</template>
<script lang="ts" setup>
import { ref, computed, watch, onMounted, nextTick } from 'vue'
import { ref, computed, watch, nextTick } from 'vue'
import { useCreateUser } from 'src/features/User/CreateUser'
import { FailAlert } from 'src/shared/api';
import { Loader } from 'src/shared/ui/Loader';
@@ -70,16 +70,23 @@ const agreeWithAll = computed(() => {
const html = ref()
const isLoading = ref(false)
const isGenerating = ref(false)
const loadingText = ref('Загружаем документы...')
const statementDiv = ref<any>()
const loadDocuments = async (): Promise<void> => {
// Защита от повторных вызовов
if (isGenerating.value) {
return
}
try {
isGenerating.value = true
isLoading.value = true
loadingText.value = 'Генерируем документы для подписи...'
// Генерируем все документы регистрации с бэкенда (они уже содержат HTML для отображения)
await generateAllRegistrationDocuments(registratorStore.state.selectedProgramKey)
await generateAllRegistrationDocuments(registratorStore.state.selectedProgramKey || undefined)
loadingText.value = 'Заполняем заявление...'
@@ -93,6 +100,8 @@ const loadDocuments = async (): Promise<void> => {
} catch (e: any) {
isLoading.value = false
FailAlert(e)
} finally {
isGenerating.value = false
}
}
@@ -104,17 +113,12 @@ const back = () => {
}
}
onMounted(() => {
if (registratorStore.state.step === registratorStore.steps.ReadStatement) {
loadDocuments()
}
})
// Используем только watch с immediate: true вместо onMounted + watch
watch(() => registratorStore.state.step, (value: number) => {
if (value === registratorStore.steps.ReadStatement) {
loadDocuments()
}
})
}, { immediate: true })
const statuteLink = computed(() => {
return systemStore.info.vars?.statute_link || ''
@@ -25,26 +25,30 @@ import { FailAlert } from 'src/shared/api';
import { useSystemStore } from 'src/entities/System/model';
const { info } = useSystemStore()
import { computed, onMounted, watch } from 'vue'
import { computed, watch, ref } from 'vue'
import { BranchSelector } from 'src/shared/ui/BranchSelector';
const store = useRegistratorStore()
const branchStore = useBranchStore()
const isLoading = ref(false)
const load = async () => {
if (store.isStep('SelectBranch'))
try{
await branchStore.loadPublicBranches({ coopname: info.coopname })
} catch(e: any){
FailAlert(e)
}
if (!store.isStep('SelectBranch') || isLoading.value) return;
try {
isLoading.value = true
await branchStore.loadPublicBranches({ coopname: info.coopname })
} catch(e: any) {
FailAlert(e)
} finally {
isLoading.value = false
}
}
onMounted(() => load())
// Используем только watch с immediate: true вместо onMounted + watch
watch(() => store.state.step, async () => {
load()
})
}, { immediate: true })
// Массив используется без изменений
const branches = computed(() => branchStore.publicBranches)
@@ -51,7 +51,7 @@ div
</template>
<script lang="ts" setup>
import { ref, onMounted, watch } from 'vue';
import { ref, watch } from 'vue';
import { useRegistratorStore } from 'src/entities/Registrator';
import { useSystemStore } from 'src/entities/System/model';
import { Loader } from 'src/shared/ui/Loader';
@@ -63,10 +63,17 @@ const registratorStore = useRegistratorStore();
const systemStore = useSystemStore();
const isLoading = ref(false);
const isLoadingPrograms = ref(false);
const programs = ref<any[]>([]);
const loadPrograms = async () => {
// Защита от повторных вызовов
if (isLoadingPrograms.value) {
return;
}
try {
isLoadingPrograms.value = true;
isLoading.value = true;
const accountType = registratorStore.state.userData.type;
@@ -98,8 +105,11 @@ const loadPrograms = async () => {
} catch (e: any) {
console.error('Error loading programs:', e);
FailAlert(e);
// В случае ошибки все равно переходим дальше
registratorStore.next();
} finally {
isLoading.value = false;
isLoadingPrograms.value = false;
}
};
@@ -107,15 +117,10 @@ const selectProgram = (key: string) => {
registratorStore.state.selectedProgramKey = key;
};
onMounted(() => {
if (registratorStore.state.step === registratorStore.steps.SelectProgram) {
loadPrograms();
}
});
// Используем только watch с immediate: true вместо onMounted + watch
watch(() => registratorStore.state.step, (value: number) => {
if (value === registratorStore.steps.SelectProgram) {
loadPrograms();
}
});
}, { immediate: true });
</script>

Some files were not shown because too many files have changed in this diff Show More