Compare commits
41 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 81df662cfa | |||
| 845c13605d | |||
| 76b4988da8 | |||
| 3db80a30e9 | |||
| 1b457b8f87 | |||
| ba6d3d1310 | |||
| 8e01373179 | |||
| 31940461c6 | |||
| 37b0227735 | |||
| 2cfb37c5d7 | |||
| 1c605dd2a5 | |||
| b1b1775277 | |||
| 5f4281ac01 | |||
| d35e88d951 | |||
| 5360daa471 | |||
| 652f7c722e | |||
| a2b132b457 | |||
| a6cc71d6a3 | |||
| b67581ce80 | |||
| 024d46ff64 | |||
| 9030d28c6b | |||
| 6e0e68c1e7 | |||
| c079785019 | |||
| bbc6453b24 | |||
| b5eb0ea51f | |||
| a072364d00 | |||
| ed9b0879d5 | |||
| c94043e77f | |||
| c1a8f3e637 | |||
| 15fdf35dd3 | |||
| 79b89695d1 | |||
| 7b27d0805c | |||
| e089dbd3fb | |||
| 5230fab7c6 | |||
| 345aadbd6a | |||
| 33f1ac1a15 | |||
| 5de7e7c65b | |||
| bfbcd2d57f | |||
| ff5df68bce | |||
| a948f07a1b | |||
| 85bd044cd6 |
@@ -390,6 +390,18 @@ jobs:
|
||||
|
||||
- run: pnpm install --frozen-lockfile
|
||||
- run: pnpm lerna run build
|
||||
|
||||
# schema.gql не коммитится и не создаётся ничем в этом job'е, а
|
||||
# @coopenomics/sdk's prepublishOnly (pnpm run docs → generate-index-comments.ts)
|
||||
# читает components/controller/schema.gql и падает ENOENT без него — валя весь
|
||||
# lerna publish (атомарный) молча с 30 мая. Скрипт изолированный (GraphQLSchemaBuilderModule,
|
||||
# без TypeORM/Mongo/Redis) — не требует БД, но типизация controller'а резолвится через
|
||||
# dist соседних workspace-пакетов (cooptypes/factory/notifications/inter) — ОБЯЗАТЕЛЬНО
|
||||
# после `lerna run build`, иначе ts-node падает на их устаревших dist-типах.
|
||||
- name: Generate controller schema.gql (required by sdk prepublishOnly)
|
||||
working-directory: components/controller
|
||||
run: pnpm run generate-schema
|
||||
|
||||
- run: pnpm lerna publish from-package --yes
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
@@ -45,6 +45,14 @@ RUN corepack enable && npm install -g lerna --no-fund --no-audit
|
||||
# выкидывает node_modules/dist/.git, поэтому `COPY .` лёгкий.
|
||||
COPY . .
|
||||
|
||||
# electron (devDep desktop → @vue/devtools) и puppeteer нигде в build/runtime
|
||||
# не запускаются (unbuild/tsc/vite их не трогают) — только их JS-обёртка нужна
|
||||
# для типов. Их postinstall тянет тяжёлые бинарники (Electron zip с GitHub
|
||||
# Releases, Chromium) по сети — периодически валит билд TLS-таймаутом.
|
||||
# Отключаем сами загрузки, пакеты всё равно ставятся.
|
||||
ENV ELECTRON_SKIP_BINARY_DOWNLOAD=1 \
|
||||
PUPPETEER_SKIP_DOWNLOAD=true
|
||||
|
||||
# Используем существующий lockfile. Если он не совпадает с workspace —
|
||||
# падать сразу, не дрейфовать незаметно.
|
||||
RUN pnpm install --frozen-lockfile
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@coopenomics/blago-cli",
|
||||
"version": "2026.6.24",
|
||||
"version": "2026.7.21",
|
||||
"description": "CLI синхронизации артефактов Благорост с бэкендом через @coopenomics/sdk",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
|
||||
@@ -35,7 +35,14 @@ RUN npm install -g pnpm@10.33.0
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN pnpm install --frozen-lockfile --filter "@coopenomics/boot..."
|
||||
# --ignore-scripts + точечный rebuild: --filter ограничивает только то, какие
|
||||
# проекты получают node_modules, но НЕ то, для каких пакетов общего lockfile
|
||||
# store выполняет lifecycle-скрипты при --frozen-lockfile. Без --ignore-scripts
|
||||
# сюда тянутся electron/puppeteer (из @coopenomics/desktop/notifications,
|
||||
# boot их не использует) — тяжёлые сетевые загрузки, изредка валящие билд по
|
||||
# TLS-таймауту. rebuild --filter уже честно скопирован на подграф boot.
|
||||
RUN pnpm install --frozen-lockfile --filter "@coopenomics/boot..." --ignore-scripts \
|
||||
&& pnpm rebuild --filter "@coopenomics/boot..."
|
||||
|
||||
# Билд графа: cooptypes/factory собирают свои dist/, boot собирает
|
||||
# свой bundle с inlined workspace-deps.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@coopenomics/boot",
|
||||
"type": "module",
|
||||
"version": "2026.6.24",
|
||||
"version": "2026.7.21",
|
||||
"private": true,
|
||||
"packageManager": "pnpm@9.0.6",
|
||||
"description": "CLI-утилита инициализации блокчейна и кооператива",
|
||||
|
||||
@@ -76,7 +76,7 @@ export class CooperativeClass {
|
||||
await this.blockchain.createProgram({
|
||||
coopname,
|
||||
username: coopname,
|
||||
type: 'blagorost',
|
||||
type: 'capital',
|
||||
title: 'Целевая потребительская программа "Благорост"',
|
||||
announce: '',
|
||||
description: '',
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Фаза 01 — программы УХД (id=3, type='generator') и Благорост
|
||||
* (id=4, type='blagorost') + capital::setconfig.
|
||||
* (id=4, type='capital') + capital::setconfig.
|
||||
*
|
||||
* Идемпотентно: программы создаются только если их ещё нет в `programs`-таблице,
|
||||
* setconfig — только если `state` пуст.
|
||||
@@ -88,7 +88,7 @@ export async function phase01(): Promise<void> {
|
||||
fixed_membership_contribution: '0.0000 RUB',
|
||||
membership_percent_fee: '0',
|
||||
meta: '',
|
||||
type: 'blagorost',
|
||||
type: 'capital',
|
||||
}
|
||||
await blockchain.api.transact({
|
||||
actions: [{
|
||||
|
||||
@@ -300,8 +300,8 @@ async function ensureAgreementsAndWallet(blockchain: Blockchain, fix: IFixture):
|
||||
}
|
||||
// Согласие с ЦПП «Благорост» — для инвестиций в программу/компонент.
|
||||
// Блокировка идёт по своему пути; для безопасности дёргаем при первом запуске.
|
||||
await signAgreement(blockchain, fix.username, 'blagorost').catch((e) => {
|
||||
log(`signAgreement blagorost для ${fix.username} — пропуск (${(e as Error).message?.slice(0, 80)})`)
|
||||
await signAgreement(blockchain, fix.username, 'capital').catch((e) => {
|
||||
log(`signAgreement capital для ${fix.username} — пропуск (${(e as Error).message?.slice(0, 80)})`)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -234,7 +234,7 @@ describe('тест контракта CAPITAL', () => {
|
||||
fixed_membership_contribution: '0.0000 RUB',
|
||||
membership_percent_fee: '0',
|
||||
meta: '',
|
||||
type: 'blagorost',
|
||||
type: 'capital',
|
||||
}
|
||||
|
||||
const result = await blockchain.api.transact(
|
||||
|
||||
@@ -213,7 +213,7 @@ describe('тест контракта CAPITAL', () => {
|
||||
fixed_membership_contribution: '0.0000 RUB',
|
||||
membership_percent_fee: '0',
|
||||
meta: '',
|
||||
type: 'blagorost',
|
||||
type: 'capital',
|
||||
}
|
||||
|
||||
const result = await blockchain.api.transact(
|
||||
|
||||
@@ -12,6 +12,6 @@ export const capitalDraftId = 1000
|
||||
|
||||
export const ratePerHour = '1000.0000 RUB'
|
||||
export const sourceProgramName = 'generator'
|
||||
export const capitalProgramName = 'blagorost'
|
||||
export const capitalProgramName = 'capital'
|
||||
// ledger2::accounts.id хранит счёт со смещением *1000 (80 → 80000).
|
||||
export const circulationAccountId = 80_000
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@coopenomics/cleos",
|
||||
"version": "2026.6.24",
|
||||
"version": "2026.7.21",
|
||||
"private": true,
|
||||
"description": "Обёртка над кошельком cleos для EOSIO блокчейна",
|
||||
"scripts": {
|
||||
|
||||
@@ -80,7 +80,7 @@ static constexpr eosio::name _wallet_program = "wallet"_n; ///< Главный
|
||||
static constexpr eosio::name _marketplace_program = "marketplace"_n; ///< Кошелёк программы "Маркетплейс"
|
||||
|
||||
static constexpr eosio::name _source_program = "generator"_n; ///< Кошелёк для генерации по договору УХД
|
||||
static constexpr eosio::name _capital_program = "blagorost"_n; ///< Кошелёк программы "Благорост"
|
||||
static constexpr eosio::name _capital_program = "capital"_n; ///< Кошелёк программы "Благорост"
|
||||
|
||||
|
||||
static const std::set<eosio::name> soviet_actions = {
|
||||
|
||||
@@ -12,14 +12,22 @@
|
||||
void soviet::migrate() {
|
||||
require_auth(_soviet); // Проверяем авторизацию
|
||||
|
||||
// Миграция program_type для кооператива voskhod, программа id=4 -> blagorost
|
||||
eosio::name coopname = "voskhod"_n;
|
||||
programs_index programs(_soviet, coopname.value);
|
||||
auto program_it = programs.find(4);
|
||||
if (program_it != programs.end() && program_it->program_type != "blagorost"_n) {
|
||||
programs.modify(program_it, _soviet, [&](auto &pr) {
|
||||
pr.program_type = "blagorost"_n;
|
||||
});
|
||||
// Откат program_type для кооператива voskhod, программа id=4: blagorost -> capital.
|
||||
// Предыдущая версия этой миграции переименовывала programs.program_type в
|
||||
// 'blagorost', но coagreements.type для той же программы (program_id=4) всегда
|
||||
// оставался 'capital' и никогда не переименовывался — таблицы разъехались.
|
||||
// 'capital' — каноничное значение (используется coagreements.type, contract-
|
||||
// константой _capital_program и всем controller-кодом); эта миграция приводит
|
||||
// programs.program_type к нему же. Идемпотентно.
|
||||
{
|
||||
eosio::name coopname = "voskhod"_n;
|
||||
programs_index programs(_soviet, coopname.value);
|
||||
auto program_it = programs.find(4);
|
||||
if (program_it != programs.end() && program_it->program_type != "capital"_n) {
|
||||
programs.modify(program_it, _soviet, [&](auto &pr) {
|
||||
pr.program_type = "capital"_n;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@coopenomics/contracts",
|
||||
"version": "2026.6.24",
|
||||
"version": "2026.7.21",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
+17
-20
@@ -14,34 +14,28 @@ const TARGET_COOPNAME = 'voskhod';
|
||||
const COLLECTION_NAME = 'doc_private_data';
|
||||
|
||||
const capitalProgramPrivateData = {
|
||||
approval_protocol_number: '1',
|
||||
approval_protocol_day: '15',
|
||||
approval_protocol_month: 'января',
|
||||
approval_protocol_year: '26',
|
||||
cooperative_name: 'ВОСХОД',
|
||||
cooperative_short_name: 'ПК',
|
||||
cooperative_quoted_name: '«ВОСХОД»',
|
||||
website: 'https://цифровой-кооператив.рф',
|
||||
chairman_full_name: 'Муравьев Алексей Николаевич',
|
||||
generator_program_purpose:
|
||||
'развитию информационной экосистемы взаимодействия физических и юридических лиц, на основе международных кооперативных принципов и законодательства Российской Федерации в отношении потребительских обществ (кооперативов) под названием “Кооперативная Экономика”',
|
||||
"развитию информационной экосистемы взаимодействия физических и юридических лиц, на основе международных кооперативных принципов и законодательства Российской Федерации в отношении потребительских обществ (кооперативов) – под названием “Кооперативная Экономика”, с целью самоорганизации и интеграции в социально-экономическую среду Российской Федерации., на основе международных кооперативных принципов и законодательства Российской Федерации",
|
||||
eoap_definition:
|
||||
'информационная экосистема, интегрируемая в социально-экономическую среду Российской Федерации, состоящая из комплекса программных продуктов на базе технологии распределенного реестра, обеспечивающих широкое экономическое и социальное взаимодействие физических и юридических лиц, включая нерезидентов различных юрисдикций и организационно-правовых форм, на основе международных кооперативных принципов и законодательства Российской Федерации в отношении потребительских кооперативов (обществ) под названием “Кооперативная Экономика”',
|
||||
"информационная экосистема, интегрируемая в социально-экономическую среду Российской Федерации, состоящая из комплекса программных продуктов на базе технологии распределенного реестра, обеспечивающих широкое экономическое и социальное взаимодействие физических и юридических лиц, включая нерезидентов различных юрисдикций и организационно-правовых форм, на основе международных кооперативных принципов и законодательства Российской Федерации в отношении потребительских кооперативов (обществ) под названием “Кооперативная Экономика”",
|
||||
generator_task_goal:
|
||||
'центра привлечения и интеграции передовых инновационных цифровых разработок, а также экономических и социальных методов и решений',
|
||||
idea_unit_cost: '50',
|
||||
idea_unit_cost_words: 'пятьдесят',
|
||||
"центра привлечения и интеграции передовых инновационных цифровых разработок, а также экономических и социальных методов и решений",
|
||||
idea_unit_cost:
|
||||
"50",
|
||||
idea_unit_cost_words:
|
||||
"пятьдесят",
|
||||
blagorost_goal_expansion:
|
||||
'вследствие увеличения количества Участников информационной кооперативной экосистемы - ЕОАП - для расширения и повышения социальной эффективности их экономического взаимодействия в некоммерческом формате',
|
||||
"развитии информационной экосистемы взаимодействия физических и юридических лиц, на основе международных кооперативных принципов и законодательства Российской Федерации в отношении потребительских обществ (кооперативов) – под названием “Кооперативная Экономика”, с целью самоорганизации и интеграции в социально-экономическую среду Российской Федерации",
|
||||
blagorost_goal_reason:
|
||||
"вследствие увеличения количества Участников информационной кооперативной экосистемы - ЕОАП - для расширения и повышения социальной эффективности их экономического взаимодействия в некоммерческом формате",
|
||||
blagorost_task_expansion:
|
||||
'расширение участников ЕОАП - информационной кооперативной экосистемы как центра экономического взаимодействия в некоммерческом формате',
|
||||
"расширение участников ЕОАП - информационной кооперативной экосистемы как центра экономического взаимодействия в некоммерческом формате",
|
||||
blagorost_task_development:
|
||||
'развитие ЕОАП как центра привлечения и интеграции инновационных цифровых разработок, а также экономических и социальных методов и решений',
|
||||
"развитие ЕОАП как центра привлечения и интеграции инновационных цифровых разработок, а также экономических и социальных методов и решений",
|
||||
return_source_description:
|
||||
'аппаратно-программная сеть узлов распределенного реестра в формате «СМЭВ+SWIFT», построенная на принципах самоорганизации и самофинансирования деятельности технологической инфраструктуры ЕОАП, обеспечивающей консенсус ее распределенных узлов по формированию базового продукта ЕОАП - полного цикла документооборота по синхронному взаимодействию пайщиков и кооперативов - участников экосистемы ЕОАП - через использование цифровых контрактов, с одновременным выполнением функций нотариата и учета финансовых и юридических событий',
|
||||
"аппаратно-программная сеть узлов распределенного реестра в формате «СМЭВ+SWIFT», построенная на принципах самоорганизации и самофинансирования деятельности технологической инфраструктуры ЕОАП, обеспечивающей консенсус ее распределенных узлов по формированию базового продукта ЕОАП - полного цикла документооборота по синхронному взаимодействию пайщиков и кооперативов - участников экосистемы ЕАОП - через использование цифровых контрактов, с одновременным выполнением функций нотариата и учета финансовых и юридических событий (подробнее на Сайте)",
|
||||
return_additional_source:
|
||||
'взносы пользователей ЕОАП, его отдельных программных продуктов и приложений, переданных Обществу или создаваемых в рамках Общества, которые интегрируются в ЕОАП',
|
||||
offer_template_number: '______',
|
||||
"взносы пользователей ЕОАП, его отдельных программных продуктов и приложений, переданных Обществу или создаваемых в рамках Общества, которые интегрируются в ЕОАП"
|
||||
};
|
||||
|
||||
function stableStringify(value: unknown): string {
|
||||
@@ -78,6 +72,9 @@ export default {
|
||||
try {
|
||||
mongo = await mongoose.createConnection(config.mongoose.url).asPromise();
|
||||
const db = mongo.db;
|
||||
if (!db) {
|
||||
throw new Error('Подключение к MongoDB установлено, но db не инициализирована');
|
||||
}
|
||||
const collection = db.collection(COLLECTION_NAME);
|
||||
await collection.createIndex({ hash: 1 }, { unique: true });
|
||||
await collection.updateOne(
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "create-nodejs-express-app",
|
||||
"version": "2026.6.24",
|
||||
"version": "2026.7.21",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "create-nodejs-express-app",
|
||||
"version": "2026.6.24",
|
||||
"version": "2026.7.21",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@a2seven/yoo-checkout": "^1.1.4",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@coopenomics/controller",
|
||||
"version": "2026.6.24",
|
||||
"version": "2026.7.21",
|
||||
"description": "Бэкенд GraphQL API кооператива на NestJS",
|
||||
"private": true,
|
||||
"bin": "bin/createNodejsApp.js",
|
||||
|
||||
@@ -14,6 +14,7 @@ const { spawnSync } = require('child_process');
|
||||
|
||||
const controllerRoot = path.resolve(__dirname, '..');
|
||||
const schemaPath = path.join(controllerRoot, 'schema.gql');
|
||||
const zeusConfigPath = path.join(controllerRoot, 'graphql-zeus.json');
|
||||
const sdkSrc = path.resolve(controllerRoot, '..', 'sdk', 'src');
|
||||
const localZeusDir = path.join(controllerRoot, 'zeus');
|
||||
const sdkZeusDir = path.join(sdkSrc, 'zeus');
|
||||
@@ -40,6 +41,12 @@ if (!fs.existsSync(sdkSrc)) {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// graphql-zeus 7.x: yargs кладёт path в argv.path, а CLIClass читает args._[0].
|
||||
// Без graphql-zeus.json config-maker уходит в inquirer и «зависает» без TTY.
|
||||
if (!fs.existsSync(zeusConfigPath)) {
|
||||
fs.writeFileSync(zeusConfigPath, `${JSON.stringify({ urlOrPath: './schema.gql' }, null, 2)}\n`, 'utf8');
|
||||
}
|
||||
|
||||
let zeusBin;
|
||||
let zeusIsWin;
|
||||
try {
|
||||
@@ -51,6 +58,7 @@ try {
|
||||
|
||||
const zeusArgs = ['./schema.gql', '../sdk/src', '--subscriptions', 'graphql-ws'];
|
||||
const startedAt = Date.now();
|
||||
console.log('generate-client: graphql-zeus… (5–30 с без вывода — норма, не прерывайте)');
|
||||
const result = spawnSync(zeusBin, zeusArgs, {
|
||||
stdio: 'inherit',
|
||||
cwd: controllerRoot,
|
||||
|
||||
@@ -4,15 +4,11 @@ import type { GetDocumentsInputDTO } from '../dto/get-documents-input.dto';
|
||||
import type { PaginationResultDomainInterface } from '~/domain/common/interfaces/pagination.interface';
|
||||
import type { DocumentPackageAggregateDomainInterface } from '~/domain/document/interfaces/document-package-aggregate-domain.interface';
|
||||
import type { GenerateAnyDocumentInputDTO } from '../dto/generate-any-document-input.dto';
|
||||
import { GeneratorInfrastructureService } from '~/infrastructure/generator/generator.service';
|
||||
import { DocumentDomainEntity } from '~/domain/document/entity/document-domain.entity';
|
||||
|
||||
@Injectable()
|
||||
export class DocumentService {
|
||||
constructor(
|
||||
private readonly documentInteractor: DocumentInteractor,
|
||||
private readonly generatorService: GeneratorInfrastructureService
|
||||
) {}
|
||||
constructor(private readonly documentInteractor: DocumentInteractor) {}
|
||||
|
||||
async getDocumentsAggregate(
|
||||
data: GetDocumentsInputDTO
|
||||
@@ -34,7 +30,7 @@ export class DocumentService {
|
||||
}
|
||||
|
||||
async generateAnyDocument(input: GenerateAnyDocumentInputDTO): Promise<DocumentDomainEntity> {
|
||||
return this.generatorService.generateDocument({
|
||||
return this.documentInteractor.generateDocument({
|
||||
data: input.data as any,
|
||||
options: input.options,
|
||||
});
|
||||
|
||||
+17
-9
@@ -6,7 +6,7 @@ import {
|
||||
UserCertificateDomainService,
|
||||
USER_CERTIFICATE_DOMAIN_SERVICE,
|
||||
} from '~/domain/user/services/user-certificate-domain.service';
|
||||
import { Cooperative, SovietContract, WalletContract } from 'cooptypes';
|
||||
import { Cooperative, SovietContract } from 'cooptypes';
|
||||
import { getActions } from '~/utils/getFetch';
|
||||
import type { DocumentPackageAggregateDomainInterface } from '../interfaces/document-package-aggregate-domain.interface';
|
||||
import type { StatementDetailAggregateDomainInterface } from '../interfaces/statement-detail-aggregate-domain.interface';
|
||||
@@ -298,9 +298,17 @@ export class DocumentPackageV1Aggregator {
|
||||
|
||||
/**
|
||||
* Находит on-chain действие-носитель подписанного связанного документа по doc_hash.
|
||||
* Сначала ищет в реестре соглашений совета (soviet::newagreement), затем —
|
||||
* в программных подписях кошелька (wallet::signagree). Возвращает поле document
|
||||
* найденного действия либо null.
|
||||
* Сначала ищет в реестре соглашений совета (soviet::newagreement — второе,
|
||||
* явное уведомление, которое дополнительно шлёт soviet::sndagreement), затем —
|
||||
* в общем уведомлении о завершённом документе (soviet::newresolved), которое
|
||||
* централизованно шлёт make_complete_document (lib/core/soviet/soviet.hpp) на
|
||||
* КАЖДЫЙ вызов — и из soviet::sndagreement, и из wallet::signagree — с
|
||||
* require_recipient(coopname), заданным один раз внутри newresolved.cpp.
|
||||
* Программные оферты (capital: Благорост/Генератор, program_id>0) идут через
|
||||
* wallet::signagree и не порождают newagreement, но newresolved у них есть
|
||||
* всегда — расширять notification-поверхность самого wallet::signagree не
|
||||
* нужно, единственное место для этого уже newresolved.cpp.
|
||||
* Возвращает поле document найденного действия либо null.
|
||||
*/
|
||||
private async findLinkedAgreementDocument(
|
||||
linkHash: string
|
||||
@@ -321,10 +329,10 @@ export class DocumentPackageV1Aggregator {
|
||||
const sovietDocument = sovietAgreement?.results?.[0]?.data?.document;
|
||||
if (sovietDocument) return sovietDocument;
|
||||
|
||||
const walletAgreement = await getActions(`${process.env.SIMPLE_EXPLORER_API}/get-actions`, {
|
||||
const resolvedAgreement = await getActions(`${process.env.SIMPLE_EXPLORER_API}/get-actions`, {
|
||||
filter: JSON.stringify({
|
||||
account: WalletContract.contractName.production,
|
||||
name: WalletContract.Actions.SignAgreement.actionName,
|
||||
account: SovietContract.contractName.production,
|
||||
name: SovietContract.Actions.Registry.NewResolved.actionName,
|
||||
receiver: process.env.COOPNAME,
|
||||
'data.document.doc_hash': docHashFilter,
|
||||
}),
|
||||
@@ -332,8 +340,8 @@ export class DocumentPackageV1Aggregator {
|
||||
limit: 1,
|
||||
});
|
||||
|
||||
const walletDocument = walletAgreement?.results?.[0]?.data?.document;
|
||||
if (walletDocument) return walletDocument;
|
||||
const resolvedDocument = resolvedAgreement?.results?.[0]?.data?.document;
|
||||
if (resolvedDocument) return resolvedDocument;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
import type { Cooperative } from 'cooptypes';
|
||||
|
||||
/**
|
||||
* Узкая задача пред-генерации: документ, у которого часть полей приватна и
|
||||
* хранится off-chain (`doc_data`) — `DocumentDomainService.generateDocument`
|
||||
* сохраняет `doc_data` через `saveDocData` и публикует дальше только
|
||||
* `doc_data_hash`. См. `Cooperative.Document.IDocDataRef` в cooptypes —
|
||||
* on-chain-сторона того же контракта.
|
||||
*/
|
||||
export interface GenerateDocumentWithPrivateDataDomainInterface extends Cooperative.Document.IGenerate {
|
||||
doc_data?: Record<string, unknown>;
|
||||
doc_data_hash?: string;
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import { DocumentPackageAggregator } from '../aggregators/document-package.aggre
|
||||
import { getActions } from '~/utils/getFetch';
|
||||
import { toDotNotation } from '~/utils/toDotNotation';
|
||||
import type { ISignedDocumentDomainInterface } from '../interfaces/signed-document-domain.interface';
|
||||
import type { GenerateDocumentWithPrivateDataDomainInterface } from '../interfaces/generate-document-with-private-data.interface';
|
||||
|
||||
@Injectable()
|
||||
export class DocumentDomainService {
|
||||
@@ -22,10 +23,7 @@ export class DocumentDomainService {
|
||||
) {}
|
||||
|
||||
public async generateDocument(data: GenerateDocumentDomainInterfaceWithOptions): Promise<DocumentDomainEntity> {
|
||||
const documentData = data.data as Cooperative.Document.IGenerate & {
|
||||
doc_data?: Record<string, unknown>;
|
||||
doc_data_hash?: string;
|
||||
};
|
||||
const documentData: GenerateDocumentWithPrivateDataDomainInterface = data.data;
|
||||
|
||||
if (documentData.doc_data) {
|
||||
const { doc_data, ...publicDocumentData } = documentData;
|
||||
@@ -38,7 +36,7 @@ export class DocumentDomainService {
|
||||
data: {
|
||||
...publicDocumentData,
|
||||
doc_data_hash: hash,
|
||||
} as Cooperative.Document.IGenerate,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,13 @@ export interface ExtensionDomainRepository<TConfig = any> {
|
||||
create(data: Partial<ExtensionDomainEntity<TConfig>>): Promise<ExtensionDomainEntity<TConfig>>;
|
||||
find(filter?: Partial<ExtensionDomainEntity<TConfig>>): Promise<ExtensionDomainEntity<TConfig>[]>;
|
||||
update(data: Partial<ExtensionDomainEntity<TConfig>>): Promise<ExtensionDomainEntity<TConfig>>;
|
||||
/**
|
||||
* Атомарно мёржит `patch` в jsonb-колонку `config` одним UPDATE (`config || patch`),
|
||||
* без чтения-изменения-записи всего объекта в памяти приложения. Конкурентные вызовы
|
||||
* с разными ключами patch не теряют изменения друг друга (в отличие от `update()`,
|
||||
* который перезаписывает весь config целиком и подвержен lost update при гонке).
|
||||
*/
|
||||
patchConfig(name: string, patch: Partial<TConfig>): Promise<ExtensionDomainEntity<TConfig>>;
|
||||
}
|
||||
|
||||
export const EXTENSION_REPOSITORY = Symbol('ExtensionDomainRepository'); // Создаем уникальный токен
|
||||
|
||||
@@ -60,6 +60,14 @@ export interface IAgreementConfigItem {
|
||||
* Порядок отображения на фронтенде (сортировка)
|
||||
*/
|
||||
order: number;
|
||||
|
||||
/**
|
||||
* Резолвер hash'а приватных данных документа (doc_data_hash), если шаблон
|
||||
* оферты на фабрике требует PrivateData. Заполняется только для
|
||||
* extension-оферт из AgreementRegistrationSpec; платформенные соглашения
|
||||
* его не имеют. В GraphQL DTO не попадает (поля мапятся декораторами).
|
||||
*/
|
||||
resolve_doc_data_hash?: () => Promise<string | undefined>;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -55,4 +55,18 @@ export interface AgreementRegistrationSpec {
|
||||
* реестр удаляет все записи с этим extension_name.
|
||||
*/
|
||||
extension_name: string;
|
||||
|
||||
/**
|
||||
* Опциональный резолвер hash'а приватных данных документа (doc_data_hash).
|
||||
* Если шаблон оферты на фабрике требует PrivateData (например, параметры
|
||||
* ЦПП, утверждённые советом), расширение предоставляет резолвер — ядро
|
||||
* вызывает его перед каждой генерацией документа этой оферты и передаёт
|
||||
* результат в фабрику как doc_data_hash.
|
||||
*
|
||||
* Резолвер вызывается на каждую генерацию (не кэшируется): источник
|
||||
* значения — конфигурация расширения, которая может обновляться без
|
||||
* перезапуска (совет пересохранил параметры программ). Ядро не знает,
|
||||
* откуда берётся hash — это контракт расширения с его фабричным шаблоном.
|
||||
*/
|
||||
resolve_doc_data_hash?: () => Promise<string | undefined>;
|
||||
}
|
||||
|
||||
+1
@@ -50,6 +50,7 @@ export class AgreementConfigurationService {
|
||||
link_to_statement: true,
|
||||
applicable_account_types: spec.applicable_account_types,
|
||||
order: spec.order,
|
||||
resolve_doc_data_hash: spec.resolve_doc_data_hash,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
+6
@@ -132,11 +132,17 @@ export class RegistrationDocumentsService {
|
||||
): Promise<IGeneratedRegistrationDocument> {
|
||||
this.logger.debug(`Генерация документа: ${config.id} (registry_id=${config.registry_id})`);
|
||||
|
||||
// Если шаблон оферты требует PrivateData, расширение-владелец предоставило
|
||||
// резолвер hash'а в спеке регистрации — ядро не знает ни источника значения,
|
||||
// ни того, какие registry_id этого требуют.
|
||||
const doc_data_hash = config.resolve_doc_data_hash ? await config.resolve_doc_data_hash() : undefined;
|
||||
|
||||
const document = await this.documentInteractor.generateDocument({
|
||||
data: {
|
||||
coopname,
|
||||
username,
|
||||
registry_id: config.registry_id,
|
||||
...(doc_data_hash ? { doc_data_hash } : {}),
|
||||
},
|
||||
options: {
|
||||
skip_save: false, // Сохраняем документ в базу для последующей сверки
|
||||
|
||||
@@ -36,6 +36,14 @@ export class CapitalOnboardingStepInputDTO {
|
||||
decision!: string;
|
||||
}
|
||||
|
||||
@InputType('SaveCapitalProgramDocDataInput')
|
||||
export class SaveCapitalProgramDocDataInputDTO {
|
||||
@Field(() => String)
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
doc_data_hash!: string;
|
||||
}
|
||||
|
||||
@ObjectType('CapitalOnboardingState')
|
||||
export class CapitalOnboardingStateDTO {
|
||||
@Field(() => Boolean)
|
||||
|
||||
+10
-1
@@ -26,10 +26,17 @@ import type { IConfig } from '../../capital-extension.module';
|
||||
* • при завершённом L1 — две оферты (generator/blagorost) и две
|
||||
* программы (generation/capitalization);
|
||||
* • идемпотентность гарантируется AgreementRegistryService.
|
||||
*
|
||||
* `resolveDocDataHash` — резолвер hash'а PrivateData параметров ЦПП,
|
||||
* прикрепляется к обеим офертам: их фабричные шаблоны (#996/#1000) требуют
|
||||
* doc_data. Сейчас все документы ЦПП читают единый набор параметров
|
||||
* (capital_program_doc_data_hash); если параметры разделятся по программам,
|
||||
* каждая спека получит собственный резолвер — ядро менять не потребуется.
|
||||
*/
|
||||
export function registerCapitalInAgreementRegistry(
|
||||
port: AgreementRegistrationPort,
|
||||
extensionConfig: IConfig
|
||||
extensionConfig: IConfig,
|
||||
resolveDocDataHash?: () => Promise<string | undefined>
|
||||
): boolean {
|
||||
const onboardingDone =
|
||||
extensionConfig.onboarding_generator_program_template_done &&
|
||||
@@ -52,6 +59,7 @@ export function registerCapitalInAgreementRegistry(
|
||||
applicable_account_types: [],
|
||||
order: 6,
|
||||
extension_name: CAPITAL_EXTENSION_NAME,
|
||||
resolve_doc_data_hash: resolveDocDataHash,
|
||||
});
|
||||
|
||||
port.registerAgreement({
|
||||
@@ -70,6 +78,7 @@ export function registerCapitalInAgreementRegistry(
|
||||
applicable_account_types: [],
|
||||
order: 5,
|
||||
extension_name: CAPITAL_EXTENSION_NAME,
|
||||
resolve_doc_data_hash: resolveDocDataHash,
|
||||
});
|
||||
|
||||
port.registerProgram({
|
||||
|
||||
+17
-1
@@ -3,7 +3,11 @@ import { UseGuards } from '@nestjs/common';
|
||||
import { GqlJwtAuthGuard } from '~/application/auth/guards/graphql-jwt-auth.guard';
|
||||
import { RolesGuard } from '~/application/auth/guards/roles.guard';
|
||||
import { AuthRoles } from '~/application/auth/decorators/auth.decorator';
|
||||
import { CapitalOnboardingStepInputDTO, CapitalOnboardingStateDTO } from '../dto/onboarding.dto';
|
||||
import {
|
||||
CapitalOnboardingStepInputDTO,
|
||||
CapitalOnboardingStateDTO,
|
||||
SaveCapitalProgramDocDataInputDTO,
|
||||
} from '../dto/onboarding.dto';
|
||||
import { CapitalOnboardingService } from '../services/onboarding.service';
|
||||
import { CurrentUser } from '~/application/auth/decorators/current-user.decorator';
|
||||
import type { MonoAccountDomainInterface } from '~/domain/account/interfaces/mono-account-domain.interface';
|
||||
@@ -34,4 +38,16 @@ export class CapitalOnboardingResolver {
|
||||
): Promise<CapitalOnboardingStateDTO> {
|
||||
return this.onboardingService.completeStep(data, currentUser?.username);
|
||||
}
|
||||
|
||||
@Mutation(() => CapitalOnboardingStateDTO, {
|
||||
name: 'saveCapitalProgramDocDataHash',
|
||||
description: 'Сохранить hash PrivateData параметров документов ЦПП',
|
||||
})
|
||||
@UseGuards(GqlJwtAuthGuard, RolesGuard)
|
||||
@AuthRoles(['chairman'])
|
||||
async saveProgramDocDataHash(
|
||||
@Args('data', { type: () => SaveCapitalProgramDocDataInputDTO }) data: SaveCapitalProgramDocDataInputDTO,
|
||||
): Promise<CapitalOnboardingStateDTO> {
|
||||
return this.onboardingService.saveProgramDocDataHash(data.doc_data_hash);
|
||||
}
|
||||
}
|
||||
|
||||
+6
-7
@@ -73,13 +73,12 @@ export class CapitalOnboardingEventsService {
|
||||
|
||||
const wasAlreadyDone = !!(plugin.config as any)[flagKey];
|
||||
|
||||
// Обновляем флаг завершения шага
|
||||
const updatedConfig = {
|
||||
...plugin.config,
|
||||
// Атомарный частичный UPDATE (config || patch) вместо read-modify-write всего
|
||||
// config целиком: два DecisionTrackedEvent для разных шагов, обработанные
|
||||
// конкурентно, иначе теряют флаг друг друга (lost update на общем jsonb-блобе).
|
||||
const updated = await this.extensionRepository.patchConfig('capital', {
|
||||
[flagKey]: true,
|
||||
};
|
||||
|
||||
await this.extensionRepository.update({ ...plugin, config: updatedConfig });
|
||||
} as Partial<IConfig>);
|
||||
|
||||
this.logger.info(`Флаг ${flagKey} установлен в true`);
|
||||
|
||||
@@ -89,7 +88,7 @@ export class CapitalOnboardingEventsService {
|
||||
// restartApp('capital'), чтобы initialize(config) увидел свежий config
|
||||
// и зарегистрировал оферты/программы в платформенном реестре.
|
||||
// Совет ничего не нажимает (раздел 7.1 плана C28-10, вариант (в)).
|
||||
if (!wasAlreadyDone && this.isL1Complete(updatedConfig as IConfig)) {
|
||||
if (!wasAlreadyDone && this.isL1Complete(updated.config as IConfig)) {
|
||||
this.logger.info(
|
||||
'[ONBOARDING_COMPLETED] capital L1 завершён последним шагом, эмиттим событие для auto-restart'
|
||||
);
|
||||
|
||||
+30
-12
@@ -127,21 +127,24 @@ export class CapitalOnboardingService {
|
||||
if (!plugin) throw new Error('Конфигурация расширения capital не найдена');
|
||||
const pluginConfig: CapitalOnboardingConfig = { ...plugin.config };
|
||||
|
||||
let needUpdate = false;
|
||||
const patch: Partial<CapitalOnboardingConfig> = {};
|
||||
if (!pluginConfig.onboarding_init_at) {
|
||||
pluginConfig.onboarding_init_at = new Date().toISOString();
|
||||
needUpdate = true;
|
||||
patch.onboarding_init_at = pluginConfig.onboarding_init_at;
|
||||
}
|
||||
|
||||
if (!pluginConfig.onboarding_expire_at) {
|
||||
const start = new Date(pluginConfig.onboarding_init_at);
|
||||
pluginConfig.onboarding_expire_at = computeOnboardingExpiresAt(start);
|
||||
needUpdate = true;
|
||||
patch.onboarding_expire_at = pluginConfig.onboarding_expire_at;
|
||||
}
|
||||
|
||||
if (needUpdate) {
|
||||
await this.extensionRepository.update({ ...plugin, config: pluginConfig });
|
||||
return { ...plugin, config: pluginConfig };
|
||||
if (Object.keys(patch).length > 0) {
|
||||
// Точечный merge только изменившихся полей — полная перезапись config
|
||||
// целиком (как раньше) конкурирует с параллельными записями других шагов
|
||||
// онбординга (флаги, хэши) и теряет их изменения (lost update).
|
||||
const updated = await this.extensionRepository.patchConfig('capital', patch);
|
||||
return { ...plugin, config: updated.config };
|
||||
}
|
||||
|
||||
return { ...plugin, config: pluginConfig };
|
||||
@@ -170,6 +173,21 @@ export class CapitalOnboardingService {
|
||||
return this.buildState(plugin.config);
|
||||
}
|
||||
|
||||
public async saveProgramDocDataHash(docDataHash: string): Promise<CapitalOnboardingStateDTO> {
|
||||
await this.loadPlugin();
|
||||
const normalizedHash = docDataHash.trim();
|
||||
|
||||
if (!normalizedHash) {
|
||||
throw new Error('Hash PrivateData документов ЦПП не может быть пустым');
|
||||
}
|
||||
|
||||
const updated = await this.extensionRepository.patchConfig('capital', {
|
||||
capital_program_doc_data_hash: normalizedHash,
|
||||
} as Partial<CapitalOnboardingConfig>);
|
||||
|
||||
return this.buildState(updated.config as CapitalOnboardingConfig);
|
||||
}
|
||||
|
||||
public async completeStep(data: CapitalOnboardingStepInputDTO, username: string): Promise<CapitalOnboardingStateDTO> {
|
||||
const plugin = await this.loadPlugin();
|
||||
const flagKey = this.mapStepToFlag(data.step);
|
||||
@@ -218,12 +236,12 @@ export class CapitalOnboardingService {
|
||||
document: documentForPublish,
|
||||
});
|
||||
|
||||
// Сохраняем hash в конфиге для отображения на фронтенде
|
||||
const updatedConfig = {
|
||||
...plugin.config,
|
||||
// Сохраняем hash в конфиге для отображения на фронтенде — точечный merge
|
||||
// (не полная перезапись config), чтобы не потерять конкурентно записанные
|
||||
// флаги/хэши других шагов онбординга (lost update на общем jsonb-блобе).
|
||||
const updated = await this.extensionRepository.patchConfig('capital', {
|
||||
[hashKey]: generatedDoc.hash,
|
||||
};
|
||||
await this.extensionRepository.update({ ...plugin, config: updatedConfig });
|
||||
} as Partial<CapitalOnboardingConfig>);
|
||||
|
||||
// Регистрируем правило отслеживания в фабрике
|
||||
const varsField = this.mapStepToVarsField(data.step);
|
||||
@@ -239,6 +257,6 @@ export class CapitalOnboardingService {
|
||||
},
|
||||
});
|
||||
|
||||
return this.buildState(updatedConfig);
|
||||
return this.buildState(updated.config as CapitalOnboardingConfig);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -654,7 +654,25 @@ export class CapitalPlugin extends BaseExtModule {
|
||||
// Если L1 ещё не завершён — реестр остаётся пустым, SignUp не предлагает
|
||||
// программы capital (раздел 4.1 req 44 проекта 13).
|
||||
try {
|
||||
const registered = registerCapitalInAgreementRegistry(this.agreementRegistrationPort, extensionConfig as IConfig);
|
||||
// Резолвер читает конфиг заново на каждую генерацию: совет может
|
||||
// пересохранить параметры ЦПП (новый hash) без перезапуска расширения,
|
||||
// а спека в реестре регистрируется один раз на initialize.
|
||||
const resolveCapitalProgramDocDataHash = async (): Promise<string | undefined> => {
|
||||
const ext = await this.extensionRepository.findByName(this.name);
|
||||
const hash = ext?.config?.capital_program_doc_data_hash?.trim();
|
||||
if (!hash) {
|
||||
throw new Error(
|
||||
'Параметры документов ЦПП не заполнены: отсутствует capital_program_doc_data_hash в конфигурации capital'
|
||||
);
|
||||
}
|
||||
return hash;
|
||||
};
|
||||
|
||||
const registered = registerCapitalInAgreementRegistry(
|
||||
this.agreementRegistrationPort,
|
||||
extensionConfig as IConfig,
|
||||
resolveCapitalProgramDocDataHash
|
||||
);
|
||||
if (registered) {
|
||||
this.logger.log('[CAPITAL.REGISTRY] зарегистрировано 2 оферты + 2 программы capital');
|
||||
} else {
|
||||
|
||||
@@ -23,9 +23,13 @@ export const GENERATOR_OFFER_AGREEMENT_ID = 'generator_offer';
|
||||
export const BLAGOROST_OFFER_AGREEMENT_ID = 'blagorost_offer';
|
||||
|
||||
export const GENERATOR_AGREEMENT_TYPE = 'generator';
|
||||
// On-chain имя оферты Благорост в `soviet::coagreements` — 'capital' (program_id=4).
|
||||
// Контроллер обязан слать sndagreement/signagree с этим значением, иначе
|
||||
// `get_coagreement_or_fail` падает с «Соглашение указанного типа не найдено».
|
||||
// On-chain имя оферты Благорост в `soviet::coagreements` (_capital_program в
|
||||
// lib/consts.hpp контракта) — 'capital' (program_id=4). Подтверждено прямым
|
||||
// запросом к продовой цепочке voskhod (185.70.105.87). Значение здесь ОБЯЗАНО
|
||||
// совпадать буквально с контрактной константой — иначе account.adapter.ts не
|
||||
// находит coagreement по type, программный signagree подменяется на
|
||||
// sndagreement, и `get_coagreement_or_fail` падает с «Соглашение указанного
|
||||
// типа не найдено».
|
||||
export const BLAGOROST_AGREEMENT_TYPE = 'capital';
|
||||
|
||||
export const GENERATION_PROGRAM_KEY = 'GENERATION';
|
||||
|
||||
+8
-11
@@ -74,8 +74,6 @@ export class ChairmanOnboardingEventsService {
|
||||
return;
|
||||
}
|
||||
|
||||
const cfg = { ...plugin.config };
|
||||
|
||||
// Определяем ключ флага для обновления на основе metadata
|
||||
const flagKey = this.mapStepToFlag(step);
|
||||
|
||||
@@ -84,16 +82,15 @@ export class ChairmanOnboardingEventsService {
|
||||
return;
|
||||
}
|
||||
|
||||
const wasAlreadyDone = !!(cfg as any)[flagKey];
|
||||
const wasAlreadyDone = !!(plugin.config as any)[flagKey];
|
||||
|
||||
// Обновляем флаг выполнения шага
|
||||
const patch: Partial<IConfig> = {
|
||||
// Атомарный частичный UPDATE (config || patch) вместо read-modify-write всего
|
||||
// config целиком: несколько DecisionTrackedEvent (согласия/подписи), обработанные
|
||||
// конкурентно, иначе теряют флаг друг друга (lost update на общем jsonb-блобе) —
|
||||
// тот же класс бага, что чинили в capital-онбординге.
|
||||
const updated = await this.extensionRepository.patchConfig('chairman', {
|
||||
[flagKey]: true,
|
||||
};
|
||||
|
||||
const updatedConfig = { ...cfg, ...patch };
|
||||
const updated: ExtensionDomainEntity<IConfig> = { ...plugin, config: updatedConfig };
|
||||
await this.extensionRepository.update(updated);
|
||||
} as Partial<IConfig>);
|
||||
|
||||
this.logger.info(`Онбординг обновлён: ${flagKey} = true (решение ${result.decision_id})`);
|
||||
|
||||
@@ -101,7 +98,7 @@ export class ChairmanOnboardingEventsService {
|
||||
// завершённым, эмиттим ONBOARDING_COMPLETED_EVENT.
|
||||
// У chairman нет L2-оферт в реестре сейчас, но событие важно как
|
||||
// сигнал «L1 кооператива закрыт» — раздел 7.1 C28-10.
|
||||
if (!wasAlreadyDone && this.isL1Complete(updatedConfig as IConfig)) {
|
||||
if (!wasAlreadyDone && this.isL1Complete(updated.config as IConfig)) {
|
||||
this.logger.info(
|
||||
'[ONBOARDING_COMPLETED] chairman L1 завершён последним шагом, эмиттим событие'
|
||||
);
|
||||
|
||||
+19
-15
@@ -99,22 +99,25 @@ export class ChairmanOnboardingService {
|
||||
if (!plugin) throw new Error('Конфигурация расширения chairman не найдена');
|
||||
const config = { ...plugin.config };
|
||||
|
||||
let needUpdate = false;
|
||||
const patch: Partial<IConfig> = {};
|
||||
if (!config.onboarding_init_at) {
|
||||
config.onboarding_init_at = new Date().toISOString();
|
||||
needUpdate = true;
|
||||
patch.onboarding_init_at = config.onboarding_init_at;
|
||||
}
|
||||
|
||||
if (!config.onboarding_expire_at) {
|
||||
const start = new Date(config.onboarding_init_at);
|
||||
const expire = new Date(start.getTime() + 30 * 24 * 60 * 60 * 1000);
|
||||
config.onboarding_expire_at = expire.toISOString();
|
||||
needUpdate = true;
|
||||
patch.onboarding_expire_at = config.onboarding_expire_at;
|
||||
}
|
||||
|
||||
if (needUpdate) {
|
||||
await this.extensionRepository.update({ ...plugin, config });
|
||||
return { ...plugin, config };
|
||||
if (Object.keys(patch).length > 0) {
|
||||
// Точечный merge только изменившихся полей — полная перезапись config
|
||||
// целиком конкурирует с параллельными записями флагов/хэшей других шагов
|
||||
// онбординга и теряет их изменения (lost update на общем jsonb-блобе).
|
||||
const updated = await this.extensionRepository.patchConfig('chairman', patch);
|
||||
return { ...plugin, config: updated.config };
|
||||
}
|
||||
|
||||
return plugin;
|
||||
@@ -198,12 +201,12 @@ export class ChairmanOnboardingService {
|
||||
document: documentForPublish,
|
||||
});
|
||||
|
||||
// Сохраняем hash в конфиге для отображения на фронтенде
|
||||
const updatedConfig: IConfig = {
|
||||
...plugin.config,
|
||||
// Сохраняем hash в конфиге для отображения на фронтенде — точечный merge
|
||||
// (не полная перезапись config), чтобы не потерять конкурентно записанные
|
||||
// флаги/хэши других шагов онбординга (lost update на общем jsonb-блобе).
|
||||
const updated = await this.extensionRepository.patchConfig('chairman', {
|
||||
[hashKey]: generatedDoc.hash,
|
||||
};
|
||||
await this.extensionRepository.update({ ...plugin, config: updatedConfig });
|
||||
} as Partial<IConfig>);
|
||||
|
||||
// Регистрируем правило отслеживания в фабрике
|
||||
const varsField = this.mapStepToVarsField(data.step);
|
||||
@@ -219,7 +222,7 @@ export class ChairmanOnboardingService {
|
||||
},
|
||||
});
|
||||
|
||||
return this.buildState(updatedConfig);
|
||||
return this.buildState(updated.config);
|
||||
}
|
||||
|
||||
// Сохраняем hash общего собрания, флаг закроется после newresolved
|
||||
@@ -233,8 +236,9 @@ export class ChairmanOnboardingService {
|
||||
// Если hash не пришёл (не должно быть), не затираем существующее значение
|
||||
const meetHash = proposal_hash || plugin.config.onboarding_general_meet_hash || '';
|
||||
|
||||
const updatedConfig: IConfig = { ...plugin.config, onboarding_general_meet_hash: meetHash };
|
||||
await this.extensionRepository.update({ ...plugin, config: updatedConfig });
|
||||
const updated = await this.extensionRepository.patchConfig('chairman', {
|
||||
onboarding_general_meet_hash: meetHash,
|
||||
} as Partial<IConfig>);
|
||||
|
||||
// Регистрируем правило отслеживания общего собрания
|
||||
await this.decisionTrackingPort.registerTrackingRule({
|
||||
@@ -247,6 +251,6 @@ export class ChairmanOnboardingService {
|
||||
},
|
||||
});
|
||||
|
||||
return this.buildState(updatedConfig);
|
||||
return this.buildState(updated.config);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
|
||||
## Возможности
|
||||
|
||||
✨ **Удобное общение** - групповые чаты, личные сообщения, голосовые и видео-звонки
|
||||
🔐 **Безопасность** - сообщения зашифрованы и защищены
|
||||
👥 **Для всех участников** - каждый член кооператива автоматически получает доступ к чату пайщиков своего кооператива и общему чату кооперативной экономики.
|
||||
👥 **Для членов совета** - каждый член совета добавляется в зашифрованный чат совета своего кооператива.
|
||||
📱 **На всех устройствах** - работает на компьютере, телефоне и планшете
|
||||
⚡ **Быстрая установка** - просто установите расширение, и оно заработает
|
||||
- ✨ **Удобное общение** — групповые чаты, личные сообщения, голосовые и видео-звонки
|
||||
- 🔐 **Безопасность** — сообщения зашифрованы и защищены
|
||||
- 👥 **Для всех участников** — каждый член кооператива автоматически получает доступ к чату пайщиков своего кооператива и общему чату кооперативной экономики
|
||||
- 👥 **Для членов совета** — каждый член совета добавляется в зашифрованный чат совета своего кооператива
|
||||
- 📱 **На всех устройствах** — работает на компьютере, телефоне и планшете
|
||||
- ⚡ **Быстрая установка** — просто установите расширение, и оно заработает
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { ObjectType, Field, InputType, registerEnumType } from '@nestjs/graphql';
|
||||
import { IsOptional, IsString, IsIn, Matches, MaxLength } from 'class-validator';
|
||||
import { ReportType } from '../../domain/enums/report-type.enum';
|
||||
import { SFR_REG_NUMBER_PATTERN } from '../../domain/patterns';
|
||||
|
||||
export enum RequisiteSource {
|
||||
DATABASE = 'database',
|
||||
@@ -65,7 +66,7 @@ export class ReportRequisitesViewDTO {
|
||||
* ОКТМО — `\d{8}` или `\d{11}`
|
||||
* ОКПО — `\d{8}` или `\d{10}`
|
||||
* СНИЛС — `\d{3}-\d{3}-\d{3} \d{2}` (14 симв.) или 11 цифр подряд
|
||||
* РегНом СФР — `\d{3}-\d{3}-\d{6}` (14 симв. с тире)
|
||||
* РегНом СФР — `\d{3}-\d{3}-\d{6}` (14 симв. с тире) или 10 цифр подряд
|
||||
*/
|
||||
@InputType('UpdateReportRequisitesInput')
|
||||
export class UpdateReportRequisitesInputDTO {
|
||||
@@ -99,10 +100,10 @@ export class UpdateReportRequisitesInputDTO {
|
||||
@Matches(/^\d{8}(\d{2})?$/, { message: 'ОКПО — 8 или 10 цифр' })
|
||||
okpo?: string | null;
|
||||
|
||||
@Field(() => String, { nullable: true, description: 'Рег. номер СФР — XXX-XXX-XXXXXX' })
|
||||
@Field(() => String, { nullable: true, description: 'Рег. номер СФР — XXX-XXX-XXXXXX или 10 цифр' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@Matches(/^\d{3}-\d{3}-\d{6}$/, { message: 'Рег. номер СФР — XXX-XXX-XXXXXX (14 симв.)' })
|
||||
@Matches(SFR_REG_NUMBER_PATTERN, { message: 'Рег. номер СФР — XXX-XXX-XXXXXX или 10 цифр' })
|
||||
sfrRegNumber?: string | null;
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
|
||||
+1
-1
@@ -200,7 +200,7 @@ export class ZeroReportSignerEditsInputDTO {
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
@IsOptional()
|
||||
@Matches(SFR_REG_NUMBER_PATTERN, { message: 'Рег.номер СФР — XXX-XXX-XXXXXX' })
|
||||
@Matches(SFR_REG_NUMBER_PATTERN, { message: 'Рег.номер СФР — XXX-XXX-XXXXXX или 10 цифр' })
|
||||
sfrRegNumber!: string | null;
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
|
||||
@@ -43,8 +43,9 @@ export const OKPO_PATTERN = /^\d{8}(\d{2})?$/;
|
||||
// СНИЛС: XXX-XXX-XXX YY (с пробелом/тире) или 11 цифр подряд.
|
||||
export const SNILS_PATTERN = /^(\d{3}-\d{3}-\d{3} \d{2}|\d{11})$/;
|
||||
|
||||
// Регистрационный номер СФР: XXX-XXX-XXXXXX (3-3-6).
|
||||
export const SFR_REG_NUMBER_PATTERN = /^\d{3}-\d{3}-\d{6}$/;
|
||||
// Регистрационный номер СФР: действующий XXX-XXX-XXXXXX (3-3-6, 12 цифр)
|
||||
// или прежний — 10 цифр без разделителей (ТипРегНомерОбщ в XSD ЕФС-1).
|
||||
export const SFR_REG_NUMBER_PATTERN = /^(\d{3}-\d{3}-\d{6}|\d{10})$/;
|
||||
|
||||
/** Дата в формате `DD.MM.YYYY` (XSD `xs:date`-совместимый ФНС-формат). */
|
||||
export const DATE_DDMMYYYY_PATTERN = /^\d{2}\.\d{2}\.\d{4}$/;
|
||||
|
||||
@@ -62,15 +62,33 @@ async function bootstrap() {
|
||||
// Проверяем, был ли запущен режим миграций
|
||||
const args = process.argv.slice(2);
|
||||
if (args.includes('--migrate')) {
|
||||
await migrateData();
|
||||
process.exit(0);
|
||||
try {
|
||||
await migrateData();
|
||||
process.exit(0);
|
||||
} catch (error) {
|
||||
// migrateData() уже логирует причину; здесь важен только ненулевой
|
||||
// exit code. В production Sentry.init() ставит свой глобальный
|
||||
// process.on('unhandledRejection', ...) с дефолтным mode: 'warn' —
|
||||
// он перехватывает необработанный reject этого await и НЕ роняет
|
||||
// процесс (в отличие от штатного поведения Node без Sentry). Без
|
||||
// явного process.exit(1) здесь deploy-скрипт получал ложноположительный
|
||||
// exit code 0 при реально упавшей миграции (кейс V2.3.2 на testnet
|
||||
// 2026-07-19 — TS-ошибка компиляции миграции проглатывалась именно так).
|
||||
logger.error('Процесс миграции завершился с ошибкой, выходим с кодом 1', error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Проверяем, был ли запущен режим только миграций (для обратной совместимости)
|
||||
if (args.includes('--migrations-only')) {
|
||||
await migrateData();
|
||||
logger.info('Режим только миграций - миграции выполнены, сервер не будет запущен');
|
||||
process.exit(0);
|
||||
try {
|
||||
await migrateData();
|
||||
logger.info('Режим только миграций - миграции выполнены, сервер не будет запущен');
|
||||
process.exit(0);
|
||||
} catch (error) {
|
||||
logger.error('Процесс миграции завершился с ошибкой, выходим с кодом 1', error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Подключение к MongoDB
|
||||
|
||||
+36
@@ -57,4 +57,40 @@ export class TypeOrmExtensionDomainRepository<TConfig = any> implements Extensio
|
||||
|
||||
return existingEntity.toDomainEntity();
|
||||
}
|
||||
|
||||
async patchConfig(name: string, patch: Partial<TConfig>): Promise<ExtensionDomainEntity<TConfig>> {
|
||||
// TypeORM оборачивает .query() для UPDATE/DELETE в кортеж [rows, affectedCount]
|
||||
// (в отличие от INSERT, где возвращается голый rows-массив) — распаковываем явно,
|
||||
// иначе row оказывается всем кортежем и все поля читаются как undefined.
|
||||
const [rows] = (await this.ormRepo.query(
|
||||
`UPDATE extensions
|
||||
SET config = COALESCE(config, '{}'::jsonb) || $2::jsonb,
|
||||
updated_at = NOW()
|
||||
WHERE name = $1
|
||||
RETURNING name, enabled, config, schema_version, created_at, updated_at`,
|
||||
[name, JSON.stringify(patch)]
|
||||
)) as [
|
||||
Array<{
|
||||
name: string;
|
||||
enabled: boolean;
|
||||
config: TConfig;
|
||||
schema_version: number;
|
||||
created_at: Date;
|
||||
updated_at: Date;
|
||||
}>,
|
||||
number,
|
||||
];
|
||||
|
||||
const row = rows[0];
|
||||
if (!row) throw new Error('Расширение не найдено в установленных');
|
||||
|
||||
return new ExtensionDomainEntity<TConfig>(
|
||||
row.name,
|
||||
row.enabled,
|
||||
row.config,
|
||||
row.created_at,
|
||||
row.updated_at,
|
||||
row.schema_version
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1304,6 +1304,9 @@ export const AllTypesProps: Record<string,any> = {
|
||||
returnExpenseItem:{
|
||||
data:"ReturnExpenseItemInput"
|
||||
},
|
||||
saveCapitalProgramDocDataHash:{
|
||||
data:"SaveCapitalProgramDocDataInput"
|
||||
},
|
||||
saveReportDraft:{
|
||||
input:"SaveReportDraftInput"
|
||||
},
|
||||
@@ -1959,6 +1962,9 @@ export const AllTypesProps: Record<string,any> = {
|
||||
|
||||
},
|
||||
RoomMessageKind: "enum" as const,
|
||||
SaveCapitalProgramDocDataInput:{
|
||||
|
||||
},
|
||||
SaveReportDraftInput:{
|
||||
reportType:"ReportType"
|
||||
},
|
||||
@@ -2791,6 +2797,7 @@ export const ReturnTypes: Record<string,any> = {
|
||||
CapitalOnboardingState:{
|
||||
blagorost_offer_template_done:"Boolean",
|
||||
blagorost_provision_done:"Boolean",
|
||||
capital_program_doc_data_hash:"String",
|
||||
generation_contract_template_done:"Boolean",
|
||||
generator_offer_template_done:"Boolean",
|
||||
generator_program_template_done:"Boolean",
|
||||
@@ -4051,6 +4058,7 @@ export const ReturnTypes: Record<string,any> = {
|
||||
resetRegistration:"Account",
|
||||
restartAnnualGeneralMeet:"MeetAggregate",
|
||||
returnExpenseItem:"Transaction",
|
||||
saveCapitalProgramDocDataHash:"CapitalOnboardingState",
|
||||
saveReportDraft:"ReportDraft",
|
||||
selectBranch:"Boolean",
|
||||
sendAgreement:"Transaction",
|
||||
|
||||
@@ -3009,6 +3009,7 @@ export type ValueTypes = {
|
||||
["CapitalOnboardingState"]: AliasType<{
|
||||
blagorost_offer_template_done?:boolean | `@${string}`,
|
||||
blagorost_provision_done?:boolean | `@${string}`,
|
||||
capital_program_doc_data_hash?:boolean | `@${string}`,
|
||||
generation_contract_template_done?:boolean | `@${string}`,
|
||||
generator_offer_template_done?:boolean | `@${string}`,
|
||||
generator_program_template_done?:boolean | `@${string}`,
|
||||
@@ -7205,6 +7206,7 @@ resetKey?: [{ data: ValueTypes["ResetKeyInput"] | Variable<any, string>},boolean
|
||||
resetRegistration?:ValueTypes["Account"],
|
||||
restartAnnualGeneralMeet?: [{ data: ValueTypes["RestartAnnualGeneralMeetInput"] | Variable<any, string>},ValueTypes["MeetAggregate"]],
|
||||
returnExpenseItem?: [{ data: ValueTypes["ReturnExpenseItemInput"] | Variable<any, string>},ValueTypes["Transaction"]],
|
||||
saveCapitalProgramDocDataHash?: [{ data: ValueTypes["SaveCapitalProgramDocDataInput"] | Variable<any, string>},ValueTypes["CapitalOnboardingState"]],
|
||||
saveReportDraft?: [{ input: ValueTypes["SaveReportDraftInput"] | Variable<any, string>},ValueTypes["ReportDraft"]],
|
||||
selectBranch?: [{ data: ValueTypes["SelectBranchInput"] | Variable<any, string>},boolean | `@${string}`],
|
||||
sendAgreement?: [{ data: ValueTypes["SendAgreementInput"] | Variable<any, string>},ValueTypes["Transaction"]],
|
||||
@@ -9459,6 +9461,9 @@ validateReportEdits?: [{ editsJson: string | Variable<any, string>, reportType:
|
||||
};
|
||||
/** Тип сообщения в истории комнаты Matrix (текст или расшифрованное аудио) */
|
||||
["RoomMessageKind"]:RoomMessageKind;
|
||||
["SaveCapitalProgramDocDataInput"]: {
|
||||
doc_data_hash: string | Variable<any, string>
|
||||
};
|
||||
["SaveReportDraftInput"]: {
|
||||
editedFields: Array<string> | Variable<any, string>,
|
||||
editsJson: string | Variable<any, string>,
|
||||
@@ -12342,6 +12347,7 @@ export type ResolverInputTypes = {
|
||||
["CapitalOnboardingState"]: AliasType<{
|
||||
blagorost_offer_template_done?:boolean | `@${string}`,
|
||||
blagorost_provision_done?:boolean | `@${string}`,
|
||||
capital_program_doc_data_hash?:boolean | `@${string}`,
|
||||
generation_contract_template_done?:boolean | `@${string}`,
|
||||
generator_offer_template_done?:boolean | `@${string}`,
|
||||
generator_program_template_done?:boolean | `@${string}`,
|
||||
@@ -16439,6 +16445,7 @@ resetKey?: [{ data: ResolverInputTypes["ResetKeyInput"]},boolean | `@${string}`]
|
||||
resetRegistration?:ResolverInputTypes["Account"],
|
||||
restartAnnualGeneralMeet?: [{ data: ResolverInputTypes["RestartAnnualGeneralMeetInput"]},ResolverInputTypes["MeetAggregate"]],
|
||||
returnExpenseItem?: [{ data: ResolverInputTypes["ReturnExpenseItemInput"]},ResolverInputTypes["Transaction"]],
|
||||
saveCapitalProgramDocDataHash?: [{ data: ResolverInputTypes["SaveCapitalProgramDocDataInput"]},ResolverInputTypes["CapitalOnboardingState"]],
|
||||
saveReportDraft?: [{ input: ResolverInputTypes["SaveReportDraftInput"]},ResolverInputTypes["ReportDraft"]],
|
||||
selectBranch?: [{ data: ResolverInputTypes["SelectBranchInput"]},boolean | `@${string}`],
|
||||
sendAgreement?: [{ data: ResolverInputTypes["SendAgreementInput"]},ResolverInputTypes["Transaction"]],
|
||||
@@ -18609,6 +18616,9 @@ validateReportEdits?: [{ editsJson: string, reportType: ResolverInputTypes["Repo
|
||||
};
|
||||
/** Тип сообщения в истории комнаты Matrix (текст или расшифрованное аудио) */
|
||||
["RoomMessageKind"]:RoomMessageKind;
|
||||
["SaveCapitalProgramDocDataInput"]: {
|
||||
doc_data_hash: string
|
||||
};
|
||||
["SaveReportDraftInput"]: {
|
||||
editedFields: Array<string>,
|
||||
editsJson: string,
|
||||
@@ -21418,6 +21428,7 @@ export type ModelTypes = {
|
||||
["CapitalOnboardingState"]: {
|
||||
blagorost_offer_template_done: boolean,
|
||||
blagorost_provision_done: boolean,
|
||||
capital_program_doc_data_hash?: string | undefined | null,
|
||||
generation_contract_template_done: boolean,
|
||||
generator_offer_template_done: boolean,
|
||||
generator_program_template_done: boolean,
|
||||
@@ -25716,7 +25727,7 @@ export type ModelTypes = {
|
||||
|
||||
Требуемые роли: chairman, member. */
|
||||
generateExpenseProposalStatementDocument: ModelTypes["GeneratedDocument"],
|
||||
/** Сгенерировать протокол решения по предложенно�� повестке
|
||||
/** Сгенерировать протокол решения по предложенной повестке
|
||||
|
||||
Требуемые роли: chairman, member. */
|
||||
generateFreeDecision: ModelTypes["GeneratedDocument"],
|
||||
@@ -25870,6 +25881,10 @@ export type ModelTypes = {
|
||||
|
||||
Требуемые роли: chairman, member, user. */
|
||||
returnExpenseItem: ModelTypes["Transaction"],
|
||||
/** Сохранить hash PrivateData параметров документов ЦПП
|
||||
|
||||
Требуемые роли: chairman. */
|
||||
saveCapitalProgramDocDataHash: ModelTypes["CapitalOnboardingState"],
|
||||
/** Сохранить/обновить черновик формы отчёта (upsert по owner+type+year+period)
|
||||
|
||||
Требуемые роли: chairman. */
|
||||
@@ -28235,6 +28250,9 @@ export type ModelTypes = {
|
||||
return_amount: string
|
||||
};
|
||||
["RoomMessageKind"]:RoomMessageKind;
|
||||
["SaveCapitalProgramDocDataInput"]: {
|
||||
doc_data_hash: string
|
||||
};
|
||||
["SaveReportDraftInput"]: {
|
||||
editedFields: Array<string>,
|
||||
editsJson: string,
|
||||
@@ -29210,7 +29228,10 @@ export type ModelTypes = {
|
||||
}
|
||||
|
||||
export type GraphQLTypes = {
|
||||
["AcceptChildOrderInput"]: {
|
||||
// ------------------------------------------------------;
|
||||
// THIS FILE WAS AUTOMATICALLY GENERATED (DO NOT MODIFY);
|
||||
// ------------------------------------------------------;
|
||||
["AcceptChildOrderInput"]: {
|
||||
/** Имя аккаунта кооператива */
|
||||
coopname: string,
|
||||
/** Подписанное заявление на имущественный паевый взнос */
|
||||
@@ -31113,6 +31134,7 @@ export type GraphQLTypes = {
|
||||
__typename: "CapitalOnboardingState",
|
||||
blagorost_offer_template_done: boolean,
|
||||
blagorost_provision_done: boolean,
|
||||
capital_program_doc_data_hash?: string | undefined | null,
|
||||
generation_contract_template_done: boolean,
|
||||
generator_offer_template_done: boolean,
|
||||
generator_program_template_done: boolean,
|
||||
@@ -35632,7 +35654,7 @@ export type GraphQLTypes = {
|
||||
|
||||
Требуемые роли: chairman, member. */
|
||||
generateExpenseProposalStatementDocument: GraphQLTypes["GeneratedDocument"],
|
||||
/** Сгенерировать протокол решения по предложенно�� повестке
|
||||
/** Сгенерировать протокол решения по предложенной повестке
|
||||
|
||||
Требуемые роли: chairman, member. */
|
||||
generateFreeDecision: GraphQLTypes["GeneratedDocument"],
|
||||
@@ -35786,6 +35808,10 @@ export type GraphQLTypes = {
|
||||
|
||||
Требуемые роли: chairman, member, user. */
|
||||
returnExpenseItem: GraphQLTypes["Transaction"],
|
||||
/** Сохранить hash PrivateData параметров документов ЦПП
|
||||
|
||||
Требуемые роли: chairman. */
|
||||
saveCapitalProgramDocDataHash: GraphQLTypes["CapitalOnboardingState"],
|
||||
/** Сохранить/обновить черновик формы отчёта (upsert по owner+type+year+period)
|
||||
|
||||
Требуемые роли: chairman. */
|
||||
@@ -38346,6 +38372,9 @@ export type GraphQLTypes = {
|
||||
};
|
||||
/** Тип сообщения в истории комнаты Matrix (текст или расшифрованное аудио) */
|
||||
["RoomMessageKind"]: RoomMessageKind;
|
||||
["SaveCapitalProgramDocDataInput"]: {
|
||||
doc_data_hash: string
|
||||
};
|
||||
["SaveReportDraftInput"]: {
|
||||
editedFields: Array<string>,
|
||||
editsJson: string,
|
||||
@@ -40165,6 +40194,7 @@ type ZEUS_VARIABLES = {
|
||||
["ReturnByMoneySignedMetaDocumentInput"]: ValueTypes["ReturnByMoneySignedMetaDocumentInput"];
|
||||
["ReturnExpenseItemInput"]: ValueTypes["ReturnExpenseItemInput"];
|
||||
["RoomMessageKind"]: ValueTypes["RoomMessageKind"];
|
||||
["SaveCapitalProgramDocDataInput"]: ValueTypes["SaveCapitalProgramDocDataInput"];
|
||||
["SaveReportDraftInput"]: ValueTypes["SaveReportDraftInput"];
|
||||
["SbpDataInput"]: ValueTypes["SbpDataInput"];
|
||||
["SearchDocumentsInput"]: ValueTypes["SearchDocumentsInput"];
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "cooptypes",
|
||||
"type": "module",
|
||||
"version": "2026.6.24",
|
||||
"version": "2026.7.21",
|
||||
"description": "Shared TypeScript типы и интерфейсы экосистемы кооперативов",
|
||||
"author": "Alex Ant <dacom.dark.sun@gmail.com>",
|
||||
"license": "MIT",
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,49 +1,53 @@
|
||||
export interface CapitalProgramPrivateData {
|
||||
approval_protocol_number: string
|
||||
approval_protocol_day: string
|
||||
approval_protocol_month: string
|
||||
approval_protocol_year: string
|
||||
cooperative_name: string
|
||||
cooperative_short_name: string
|
||||
cooperative_quoted_name: string
|
||||
website: string
|
||||
chairman_full_name: string
|
||||
generator_program_purpose: string
|
||||
eoap_definition: string
|
||||
generator_task_goal: string
|
||||
idea_unit_cost: string
|
||||
idea_unit_cost_words: string
|
||||
blagorost_goal_expansion: string
|
||||
blagorost_goal_reason: string
|
||||
blagorost_task_expansion: string
|
||||
blagorost_task_development: string
|
||||
return_source_description: string
|
||||
return_additional_source: string
|
||||
offer_template_number: string
|
||||
}
|
||||
|
||||
export interface CapitalApprovalHeader {
|
||||
protocol_number: string
|
||||
protocol_day_month_year: string
|
||||
}
|
||||
|
||||
export const CAPITAL_APPROVAL_PLACEHOLDER: CapitalApprovalHeader = {
|
||||
protocol_number: '______',
|
||||
protocol_day_month_year: '«___» __________ 20__ г.',
|
||||
}
|
||||
|
||||
export const capitalProgramPrivateDataRequiredFields = [
|
||||
'approval_protocol_number',
|
||||
'approval_protocol_day',
|
||||
'approval_protocol_month',
|
||||
'approval_protocol_year',
|
||||
'cooperative_name',
|
||||
'cooperative_short_name',
|
||||
'cooperative_quoted_name',
|
||||
'website',
|
||||
'chairman_full_name',
|
||||
'generator_program_purpose',
|
||||
'eoap_definition',
|
||||
'generator_task_goal',
|
||||
'idea_unit_cost',
|
||||
'idea_unit_cost_words',
|
||||
'blagorost_goal_expansion',
|
||||
'blagorost_goal_reason',
|
||||
'blagorost_task_expansion',
|
||||
'blagorost_task_development',
|
||||
'return_source_description',
|
||||
'return_additional_source',
|
||||
'offer_template_number',
|
||||
] as const
|
||||
|
||||
export function resolveCapitalApprovalHeader(
|
||||
decision?: { protocol_number?: string; protocol_day_month_year?: string } | null,
|
||||
): CapitalApprovalHeader {
|
||||
const protocol_number = decision?.protocol_number?.trim()
|
||||
const protocol_day_month_year = decision?.protocol_day_month_year?.trim()
|
||||
|
||||
return {
|
||||
protocol_number: protocol_number || CAPITAL_APPROVAL_PLACEHOLDER.protocol_number,
|
||||
protocol_day_month_year: protocol_day_month_year || CAPITAL_APPROVAL_PLACEHOLDER.protocol_day_month_year,
|
||||
}
|
||||
}
|
||||
|
||||
export function requireCapitalProgramPrivateData(
|
||||
payload: CapitalProgramPrivateData | null | undefined,
|
||||
registry_id: number,
|
||||
|
||||
@@ -22,7 +22,20 @@ const completeStep = async (
|
||||
return state;
|
||||
};
|
||||
|
||||
const saveProgramDocDataHash = async (
|
||||
data: Mutations.Capital.SaveCapitalProgramDocDataHash.IInput['data'],
|
||||
) => {
|
||||
const { [Mutations.Capital.SaveCapitalProgramDocDataHash.name]: state } = await client.Mutation(
|
||||
Mutations.Capital.SaveCapitalProgramDocDataHash.mutation,
|
||||
{
|
||||
variables: { data },
|
||||
},
|
||||
);
|
||||
return state;
|
||||
};
|
||||
|
||||
export const api = {
|
||||
loadOnboardingState,
|
||||
completeStep,
|
||||
saveProgramDocDataHash,
|
||||
};
|
||||
|
||||
+83
@@ -0,0 +1,83 @@
|
||||
export const DOC_WIZARD_STEP_GENERATOR = 'doc_generator';
|
||||
export const DOC_WIZARD_STEP_BLAGOROST = 'doc_blagorost';
|
||||
|
||||
export const DOC_WIZARD_REGISTRY: Record<string, number> = {
|
||||
[DOC_WIZARD_STEP_GENERATOR]: 994,
|
||||
[DOC_WIZARD_STEP_BLAGOROST]: 998,
|
||||
};
|
||||
|
||||
export const DOC_WIZARD_STEP_ORDER = [DOC_WIZARD_STEP_GENERATOR, DOC_WIZARD_STEP_BLAGOROST] as const;
|
||||
|
||||
export type DocWizardStepKey = (typeof DOC_WIZARD_STEP_ORDER)[number];
|
||||
|
||||
export function isDocWizardStep(key: string): key is DocWizardStepKey {
|
||||
return (DOC_WIZARD_STEP_ORDER as readonly string[]).includes(key);
|
||||
}
|
||||
|
||||
/** Группа 1: положение ГЕНЕРАТОР, шаблон договора, положение БЛАГОРОСТ — объявляются параллельно. */
|
||||
export const COUNCIL_GROUP_FOUNDATION = 'council_foundation';
|
||||
/** Группа 2: оферты ГЕНЕРАТОР и БЛАГОРОСТ — объявляются параллельно, каждая после своего положения. */
|
||||
export const COUNCIL_GROUP_OFFERS = 'council_offers';
|
||||
|
||||
/** @deprecated Миграция черновиков stepper → COUNCIL_GROUP_OFFERS */
|
||||
export const COUNCIL_GROUP_GENERATOR_OFFER = 'council_generator_offer';
|
||||
/** @deprecated Миграция черновиков stepper → COUNCIL_GROUP_OFFERS */
|
||||
export const COUNCIL_GROUP_BLAGOROST_OFFER = 'council_blagorost_offer';
|
||||
|
||||
export const COUNCIL_STEP_GROUP_ORDER = [
|
||||
COUNCIL_GROUP_FOUNDATION,
|
||||
COUNCIL_GROUP_OFFERS,
|
||||
] as const;
|
||||
|
||||
export type CouncilGroupStepKey = (typeof COUNCIL_STEP_GROUP_ORDER)[number];
|
||||
|
||||
export const COUNCIL_STEP_IDS_BY_GROUP: Record<CouncilGroupStepKey, readonly string[]> = {
|
||||
[COUNCIL_GROUP_FOUNDATION]: [
|
||||
'generator_program_template',
|
||||
'generation_contract_template',
|
||||
'blagorost_program',
|
||||
],
|
||||
[COUNCIL_GROUP_OFFERS]: ['generator_offer_template', 'blagorost_offer_template'],
|
||||
};
|
||||
|
||||
export const COUNCIL_WIZARD_STEP_META: Record<CouncilGroupStepKey, { label: string; description: string }> = {
|
||||
[COUNCIL_GROUP_FOUNDATION]: {
|
||||
label: 'Утверждение положений и шаблонов ЦПП',
|
||||
description:
|
||||
'Положение «ГЕНЕРАТОР», шаблон договора участия в хозяйственной деятельности и положение «БЛАГОРОСТ»',
|
||||
},
|
||||
[COUNCIL_GROUP_OFFERS]: {
|
||||
label: 'Утверждение оферт ЦПП',
|
||||
description: 'Оферты «ГЕНЕРАТОР» и «БЛАГОРОСТ» для пайщиков',
|
||||
},
|
||||
};
|
||||
|
||||
export function isCouncilGroupStep(key: string): key is CouncilGroupStepKey {
|
||||
return (COUNCIL_STEP_GROUP_ORDER as readonly string[]).includes(key);
|
||||
}
|
||||
|
||||
export function resolveCouncilGroupForStepId(stepId: string): CouncilGroupStepKey | null {
|
||||
for (const groupKey of COUNCIL_STEP_GROUP_ORDER) {
|
||||
if (COUNCIL_STEP_IDS_BY_GROUP[groupKey].includes(stepId)) {
|
||||
return groupKey;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function normalizeWizardStepKey(key: string): string {
|
||||
if (isDocWizardStep(key)) {
|
||||
return key;
|
||||
}
|
||||
if (key === COUNCIL_GROUP_GENERATOR_OFFER || key === COUNCIL_GROUP_BLAGOROST_OFFER) {
|
||||
return COUNCIL_GROUP_OFFERS;
|
||||
}
|
||||
if (isCouncilGroupStep(key)) {
|
||||
return key;
|
||||
}
|
||||
return resolveCouncilGroupForStepId(key) ?? key;
|
||||
}
|
||||
|
||||
export function getCouncilStepIdsForGroup(groupKey: CouncilGroupStepKey): readonly string[] {
|
||||
return COUNCIL_STEP_IDS_BY_GROUP[groupKey];
|
||||
}
|
||||
+71
@@ -0,0 +1,71 @@
|
||||
export type EditableFieldKey =
|
||||
| 'generator_program_purpose'
|
||||
| 'eoap_definition'
|
||||
| 'generator_task_goal'
|
||||
| 'idea_unit_cost'
|
||||
| 'idea_unit_cost_words'
|
||||
| 'blagorost_goal_expansion'
|
||||
| 'blagorost_goal_reason'
|
||||
| 'blagorost_task_expansion'
|
||||
| 'blagorost_task_development'
|
||||
| 'return_source_description'
|
||||
| 'return_additional_source';
|
||||
|
||||
export const PREVIEW_PLACEHOLDER = '______';
|
||||
|
||||
export const FIELD_LABELS: Record<EditableFieldKey, string> = {
|
||||
generator_program_purpose:
|
||||
'Направление ЦПП: какую экосистему или сферу деятельности развивают пайщики (название, правовая основа, цель интеграции в экономику)',
|
||||
eoap_definition:
|
||||
'Определение Платформы: что это, из чего состоит, кто участники, на какой технологии и принципах работает',
|
||||
generator_task_goal:
|
||||
'Главная задача ЦПП: чем Платформа должна стать для участников (центр привлечения чего и для чего)',
|
||||
idea_unit_cost: 'Стоимость одной Идеи в рублях — только число',
|
||||
idea_unit_cost_words: 'Та же стоимость Идеи прописью',
|
||||
blagorost_goal_expansion:
|
||||
'Зачем расширять участие в Платформе: какая потребность участников закрывается при росте сообщества',
|
||||
blagorost_goal_reason:
|
||||
'Причина роста числа участников: за счёт чего расширяется сообщество и на что это влияет',
|
||||
blagorost_task_expansion:
|
||||
'Задача расширения: как описывается рост числа участников Платформы и их взаимодействия',
|
||||
blagorost_task_development:
|
||||
'Задача развития: как описывается привлечение инноваций, разработок и решений в Платформу',
|
||||
return_source_description:
|
||||
'Основной источник возврата взносов: что создаёт экономический эффект (инфраструктура, продукт, механизм)',
|
||||
return_additional_source:
|
||||
'Дополнительные источники возврата: взносы пользователей, продукты и приложения, передаваемые или создаваемые в Обществе',
|
||||
};
|
||||
|
||||
export const GENERATOR_DOC_FIELDS: EditableFieldKey[] = [
|
||||
'generator_program_purpose',
|
||||
'eoap_definition',
|
||||
'generator_task_goal',
|
||||
'idea_unit_cost',
|
||||
'idea_unit_cost_words',
|
||||
];
|
||||
|
||||
export const BLAGOROST_DOC_FIELDS: EditableFieldKey[] = [
|
||||
'blagorost_goal_expansion',
|
||||
'blagorost_goal_reason',
|
||||
'blagorost_task_expansion',
|
||||
'blagorost_task_development',
|
||||
'return_source_description',
|
||||
'return_additional_source',
|
||||
'eoap_definition',
|
||||
];
|
||||
|
||||
export const ALL_DOC_FIELDS: EditableFieldKey[] = [
|
||||
...GENERATOR_DOC_FIELDS,
|
||||
...BLAGOROST_DOC_FIELDS.filter((key) => !GENERATOR_DOC_FIELDS.includes(key)),
|
||||
];
|
||||
|
||||
export const DOCUMENT_SECTIONS = [
|
||||
{
|
||||
registryId: 994,
|
||||
tabLabel: 'Положение ГЕНЕРАТОР',
|
||||
},
|
||||
{
|
||||
registryId: 998,
|
||||
tabLabel: 'Положение БЛАГОРОСТ',
|
||||
},
|
||||
] as const;
|
||||
@@ -15,16 +15,16 @@ interface GeneratedDocument {
|
||||
|
||||
type CapitalOnboardingStepId = Mutations.Capital.CompleteOnboardingStep.IInput['data']['step'];
|
||||
|
||||
const onboardingState = ref<CapitalOnboardingState | null>(null);
|
||||
const loading = ref(false);
|
||||
const submitting = ref(false);
|
||||
const generatingDocument = ref(false);
|
||||
const currentGeneratedDoc = ref<GeneratedDocument | null>(null);
|
||||
|
||||
export const useCapitalOnboarding = () => {
|
||||
const systemStore = useSystemStore();
|
||||
const sessionStore = useSessionStore();
|
||||
|
||||
const onboardingState = ref<CapitalOnboardingState | null>(null);
|
||||
const loading = ref(false);
|
||||
const submitting = ref(false);
|
||||
const generatingDocument = ref(false);
|
||||
const currentGeneratedDoc = ref<GeneratedDocument | null>(null);
|
||||
|
||||
// Маппинг шагов на registry_id
|
||||
const stepToRegistryId: Record<CapitalOnboardingStepId, number> = {
|
||||
'generator_program_template': 994,
|
||||
@@ -50,7 +50,7 @@ export const useCapitalOnboarding = () => {
|
||||
const registry_id = stepToRegistryId[step.id];
|
||||
const docDataHash = onboardingState.value?.capital_program_doc_data_hash;
|
||||
if (capitalProgramDocDataRegistryIds.has(registry_id) && !docDataHash) {
|
||||
throw new Error('Параметры документов ЦПП не заполнены: переустановите или обновите настройки расширения capital');
|
||||
throw new Error('Сначала заполните параметры документов ЦПП и сформируйте предпросмотр');
|
||||
}
|
||||
|
||||
const generateDocInput: Mutations.Documents.GenerateDocument.IInput = {
|
||||
@@ -158,10 +158,16 @@ export const useCapitalOnboarding = () => {
|
||||
});
|
||||
|
||||
const isOnboardingCompleted = computed(() => {
|
||||
console.log(stepsConfig.value);
|
||||
return stepsConfig.value.every(step => step.status === 'completed');
|
||||
});
|
||||
|
||||
const hasPendingCouncilDecisions = computed(() =>
|
||||
stepsConfig.value.some((step) => step.status === 'in_progress'),
|
||||
);
|
||||
|
||||
const docDataHash = computed(() => onboardingState.value?.capital_program_doc_data_hash || null);
|
||||
const isDocParamsReady = computed(() => Boolean(docDataHash.value));
|
||||
|
||||
const config = computed<ICouncilOnboardingConfig>(() => ({
|
||||
steps: stepsConfig.value,
|
||||
expireAt: expireAt.value,
|
||||
@@ -169,18 +175,27 @@ export const useCapitalOnboarding = () => {
|
||||
completionMessage: 'Все необходимые документы для работы с программой утверждены.',
|
||||
}));
|
||||
|
||||
const loadState = async () => {
|
||||
const loadState = async (options?: { silent?: boolean }) => {
|
||||
const silent = options?.silent ?? false;
|
||||
try {
|
||||
loading.value = true;
|
||||
if (!silent) {
|
||||
loading.value = true;
|
||||
}
|
||||
await systemStore.loadSystemInfo();
|
||||
onboardingState.value = await api.loadOnboardingState();
|
||||
} catch (error) {
|
||||
FailAlert(error);
|
||||
if (!silent) {
|
||||
FailAlert(error);
|
||||
}
|
||||
} finally {
|
||||
loading.value = false;
|
||||
if (!silent) {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const refreshState = () => loadState({ silent: true });
|
||||
|
||||
const handleStepClick = async (step: ICouncilOnboardingStep) => {
|
||||
try {
|
||||
const doc = await generateDocument(step);
|
||||
@@ -225,6 +240,10 @@ export const useCapitalOnboarding = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleDocParamsSaved = async () => {
|
||||
onboardingState.value = await api.loadOnboardingState();
|
||||
};
|
||||
|
||||
return {
|
||||
config,
|
||||
loading,
|
||||
@@ -232,8 +251,13 @@ export const useCapitalOnboarding = () => {
|
||||
generatingDocument,
|
||||
currentGeneratedDoc,
|
||||
isOnboardingCompleted,
|
||||
hasPendingCouncilDecisions,
|
||||
docDataHash,
|
||||
isDocParamsReady,
|
||||
loadState,
|
||||
refreshState,
|
||||
handleStepClick,
|
||||
handleStepSubmit,
|
||||
handleDocParamsSaved,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1 +1,8 @@
|
||||
export { useCapitalOnboarding } from './composable';
|
||||
export { useCapitalProgramDocParams, getRegistryIdForDocWizardStep } from './useCapitalProgramDocParams';
|
||||
export {
|
||||
DOC_WIZARD_STEP_BLAGOROST,
|
||||
DOC_WIZARD_STEP_GENERATOR,
|
||||
DOC_WIZARD_STEP_ORDER,
|
||||
isDocWizardStep,
|
||||
} from './capitalOnboardingWizard';
|
||||
|
||||
+226
@@ -0,0 +1,226 @@
|
||||
import { computed, reactive, ref } from 'vue';
|
||||
import { useQuasar } from 'quasar';
|
||||
import type { Cooperative } from 'cooptypes';
|
||||
import { useSystemStore } from 'src/entities/System/model';
|
||||
import { useSessionStore } from 'src/entities/Session';
|
||||
import { api as documentApi } from 'src/shared/lib/document/api';
|
||||
import { api } from '../api';
|
||||
import {
|
||||
ALL_DOC_FIELDS,
|
||||
BLAGOROST_DOC_FIELDS,
|
||||
FIELD_LABELS,
|
||||
GENERATOR_DOC_FIELDS,
|
||||
PREVIEW_PLACEHOLDER,
|
||||
type EditableFieldKey,
|
||||
} from './capitalProgramDocFields';
|
||||
import {
|
||||
DOC_WIZARD_STEP_BLAGOROST,
|
||||
DOC_WIZARD_STEP_GENERATOR,
|
||||
} from './capitalOnboardingWizard';
|
||||
import {
|
||||
getCapitalProgramDocParamsDraftKey,
|
||||
readCapitalProgramDocParamsDraft,
|
||||
writeCapitalProgramDocParamsDraft,
|
||||
} from './useCapitalProgramDocParamsDraft';
|
||||
|
||||
type PreviewState = {
|
||||
html: string;
|
||||
docDataHash: string;
|
||||
};
|
||||
|
||||
const DOC_FIELDS_BY_REGISTRY: Record<number, EditableFieldKey[]> = {
|
||||
994: GENERATOR_DOC_FIELDS,
|
||||
998: BLAGOROST_DOC_FIELDS,
|
||||
};
|
||||
|
||||
function createEmptyForm(): Record<EditableFieldKey, string> {
|
||||
return Object.fromEntries(ALL_DOC_FIELDS.map((key) => [key, ''])) as Record<EditableFieldKey, string>;
|
||||
}
|
||||
|
||||
export function useCapitalProgramDocParams(options?: { onSaved?: (hash: string) => void | Promise<void> }) {
|
||||
const $q = useQuasar();
|
||||
const systemStore = useSystemStore();
|
||||
const sessionStore = useSessionStore();
|
||||
|
||||
const saving = ref(false);
|
||||
const savedHash = ref('');
|
||||
const loadingRegistryId = ref<number | null>(null);
|
||||
const wizardStepKey = ref<string>(DOC_WIZARD_STEP_GENERATOR);
|
||||
|
||||
const previews = reactive<Partial<Record<number, PreviewState>>>({});
|
||||
const form = reactive<Record<EditableFieldKey, string>>(createEmptyForm());
|
||||
|
||||
const draftStorageKey = computed(() =>
|
||||
getCapitalProgramDocParamsDraftKey(systemStore.info?.coopname ?? '', sessionStore.username ?? ''),
|
||||
);
|
||||
|
||||
const editableFields = computed(() =>
|
||||
ALL_DOC_FIELDS.map((key) => ({
|
||||
key,
|
||||
label: FIELD_LABELS[key],
|
||||
})),
|
||||
);
|
||||
|
||||
function persistDraftToStorage() {
|
||||
writeCapitalProgramDocParamsDraft(draftStorageKey.value, {
|
||||
form: { ...form },
|
||||
wizardStepKey: wizardStepKey.value,
|
||||
savedHash: savedHash.value,
|
||||
previews: { ...previews },
|
||||
});
|
||||
}
|
||||
|
||||
function restoreDraftFromStorage() {
|
||||
const draft = readCapitalProgramDocParamsDraft(draftStorageKey.value);
|
||||
if (!draft) return;
|
||||
|
||||
Object.assign(form, draft.form);
|
||||
savedHash.value = draft.savedHash;
|
||||
Object.assign(previews, draft.previews);
|
||||
wizardStepKey.value = draft.wizardStepKey ?? DOC_WIZARD_STEP_GENERATOR;
|
||||
}
|
||||
|
||||
function getPreviewPayload(): Cooperative.Registry.CapitalProgramPrivateData {
|
||||
return Object.fromEntries(
|
||||
ALL_DOC_FIELDS.map((key) => [key, String(form[key] ?? '').trim() || PREVIEW_PLACEHOLDER]),
|
||||
) as Record<EditableFieldKey, string>;
|
||||
}
|
||||
|
||||
function getMissingCount(registryId: number): number {
|
||||
const fields = DOC_FIELDS_BY_REGISTRY[registryId] ?? [];
|
||||
return fields.filter((key) => !String(form[key] ?? '').trim()).length;
|
||||
}
|
||||
|
||||
function validateSection(registryId: number): string | null {
|
||||
const fields = DOC_FIELDS_BY_REGISTRY[registryId] ?? [];
|
||||
const missing = fields
|
||||
.filter((key) => !String(form[key] ?? '').trim())
|
||||
.map((key) => FIELD_LABELS[key]);
|
||||
|
||||
if (missing.length) {
|
||||
return `Заполните все поля документа: ${missing.join(', ')}`;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function validateAll(): string | null {
|
||||
const missing = editableFields.value
|
||||
.filter(({ key }) => !String(form[key] ?? '').trim())
|
||||
.map(({ label }) => label);
|
||||
|
||||
if (missing.length) {
|
||||
return `Заполните все параметры в документах: ${missing.join(', ')}`;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function getDocDataHashFromResult(result: Awaited<ReturnType<typeof documentApi.generateDocument>>): string {
|
||||
const meta = result.meta as { doc_data_hash?: string } | undefined;
|
||||
return meta?.doc_data_hash ?? '';
|
||||
}
|
||||
|
||||
async function loadPreview(registryId: number) {
|
||||
loadingRegistryId.value = registryId;
|
||||
|
||||
try {
|
||||
const result = await documentApi.generateDocument({
|
||||
coopname: systemStore.info?.coopname ?? '',
|
||||
username: sessionStore.username ?? '',
|
||||
registry_id: registryId,
|
||||
doc_data: getPreviewPayload(),
|
||||
});
|
||||
|
||||
previews[registryId] = {
|
||||
html: result.html,
|
||||
docDataHash: getDocDataHashFromResult(result),
|
||||
};
|
||||
persistDraftToStorage();
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
$q.notify({
|
||||
type: 'negative',
|
||||
message: error instanceof Error ? error.message : 'Не удалось сформировать предпросмотр',
|
||||
});
|
||||
throw error;
|
||||
} finally {
|
||||
loadingRegistryId.value = null;
|
||||
}
|
||||
}
|
||||
|
||||
async function saveParams() {
|
||||
const validationError = validateAll();
|
||||
if (validationError) {
|
||||
$q.notify({ type: 'warning', message: validationError });
|
||||
return false;
|
||||
}
|
||||
|
||||
saving.value = true;
|
||||
|
||||
try {
|
||||
await loadPreview(994);
|
||||
await loadPreview(998);
|
||||
|
||||
const hash = previews[994]?.docDataHash ?? previews[998]?.docDataHash;
|
||||
if (!hash) {
|
||||
throw new Error('Не удалось получить хеш параметров документов');
|
||||
}
|
||||
|
||||
await api.saveProgramDocDataHash({ doc_data_hash: hash });
|
||||
|
||||
savedHash.value = hash;
|
||||
persistDraftToStorage();
|
||||
await options?.onSaved?.(hash);
|
||||
|
||||
$q.notify({
|
||||
type: 'positive',
|
||||
message: 'Параметры документов ЦПП сохранены',
|
||||
});
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
$q.notify({
|
||||
type: 'negative',
|
||||
message: error instanceof Error ? error.message : 'Не удалось сохранить параметры',
|
||||
});
|
||||
return false;
|
||||
} finally {
|
||||
saving.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function ensurePreview(registryId: number) {
|
||||
if (previews[registryId]?.html) return;
|
||||
await loadPreview(registryId);
|
||||
}
|
||||
|
||||
function setWizardStepKey(key: string) {
|
||||
wizardStepKey.value = key;
|
||||
persistDraftToStorage();
|
||||
}
|
||||
|
||||
return {
|
||||
form,
|
||||
previews,
|
||||
savedHash,
|
||||
saving,
|
||||
loadingRegistryId,
|
||||
wizardStepKey,
|
||||
restoreDraftFromStorage,
|
||||
persistDraftToStorage,
|
||||
getMissingCount,
|
||||
validateSection,
|
||||
loadPreview,
|
||||
ensurePreview,
|
||||
saveParams,
|
||||
setWizardStepKey,
|
||||
};
|
||||
}
|
||||
|
||||
export function getRegistryIdForDocWizardStep(stepKey: string): number | null {
|
||||
if (stepKey === DOC_WIZARD_STEP_GENERATOR) return 994;
|
||||
if (stepKey === DOC_WIZARD_STEP_BLAGOROST) return 998;
|
||||
return null;
|
||||
}
|
||||
+105
@@ -0,0 +1,105 @@
|
||||
import { ALL_DOC_FIELDS, type EditableFieldKey } from './capitalProgramDocFields';
|
||||
|
||||
const STORAGE_PREFIX = 'capital-cpp-doc-params-draft';
|
||||
|
||||
export type CapitalProgramDocParamsPreviewState = {
|
||||
html: string;
|
||||
docDataHash: string;
|
||||
};
|
||||
|
||||
export type CapitalProgramDocParamsDraft = {
|
||||
form: Record<EditableFieldKey, string>;
|
||||
/** @deprecated используйте wizardStepKey */
|
||||
activeTab?: number;
|
||||
wizardStepKey?: string;
|
||||
savedHash: string;
|
||||
previews: Partial<Record<number, CapitalProgramDocParamsPreviewState>>;
|
||||
};
|
||||
|
||||
function isBrowser(): boolean {
|
||||
return typeof window !== 'undefined' && typeof window.localStorage !== 'undefined';
|
||||
}
|
||||
|
||||
export function getCapitalProgramDocParamsDraftKey(coopname: string, username: string): string {
|
||||
return `${STORAGE_PREFIX}:${coopname || 'unknown'}:${username || 'unknown'}`;
|
||||
}
|
||||
|
||||
function isEditableFieldKey(key: string): key is EditableFieldKey {
|
||||
return (ALL_DOC_FIELDS as readonly string[]).includes(key);
|
||||
}
|
||||
|
||||
function createEmptyForm(): Record<EditableFieldKey, string> {
|
||||
return Object.fromEntries(ALL_DOC_FIELDS.map((key) => [key, ''])) as Record<EditableFieldKey, string>;
|
||||
}
|
||||
|
||||
function normalizeForm(raw: unknown): Record<EditableFieldKey, string> | null {
|
||||
if (!raw || typeof raw !== 'object') return null;
|
||||
|
||||
const form = createEmptyForm();
|
||||
for (const [key, value] of Object.entries(raw as Record<string, unknown>)) {
|
||||
if (isEditableFieldKey(key) && typeof value === 'string') {
|
||||
form[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
return form;
|
||||
}
|
||||
|
||||
function normalizePreviews(raw: unknown): Partial<Record<number, CapitalProgramDocParamsPreviewState>> {
|
||||
const previews: Partial<Record<number, CapitalProgramDocParamsPreviewState>> = {};
|
||||
if (!raw || typeof raw !== 'object') return previews;
|
||||
|
||||
for (const [id, preview] of Object.entries(raw as Record<string, unknown>)) {
|
||||
const registryId = Number(id);
|
||||
if (!Number.isFinite(registryId) || !preview || typeof preview !== 'object') continue;
|
||||
|
||||
const html = (preview as CapitalProgramDocParamsPreviewState).html;
|
||||
const docDataHash = (preview as CapitalProgramDocParamsPreviewState).docDataHash;
|
||||
if (typeof html === 'string' && typeof docDataHash === 'string') {
|
||||
previews[registryId] = { html, docDataHash };
|
||||
}
|
||||
}
|
||||
|
||||
return previews;
|
||||
}
|
||||
|
||||
export function readCapitalProgramDocParamsDraft(storageKey: string): CapitalProgramDocParamsDraft | null {
|
||||
if (!isBrowser()) return null;
|
||||
|
||||
try {
|
||||
const raw = localStorage.getItem(storageKey);
|
||||
if (!raw) return null;
|
||||
|
||||
const parsed = JSON.parse(raw) as Partial<CapitalProgramDocParamsDraft>;
|
||||
const form = normalizeForm(parsed.form);
|
||||
if (!form) return null;
|
||||
|
||||
const legacyTab = typeof parsed.activeTab === 'number' ? parsed.activeTab : 994;
|
||||
const wizardStepKey =
|
||||
typeof parsed.wizardStepKey === 'string'
|
||||
? parsed.wizardStepKey
|
||||
: legacyTab === 998
|
||||
? 'doc_blagorost'
|
||||
: 'doc_generator';
|
||||
|
||||
return {
|
||||
form,
|
||||
wizardStepKey,
|
||||
savedHash: typeof parsed.savedHash === 'string' ? parsed.savedHash : '',
|
||||
previews: normalizePreviews(parsed.previews),
|
||||
};
|
||||
} catch {
|
||||
localStorage.removeItem(storageKey);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function writeCapitalProgramDocParamsDraft(storageKey: string, draft: CapitalProgramDocParamsDraft): void {
|
||||
if (!isBrowser()) return;
|
||||
|
||||
try {
|
||||
localStorage.setItem(storageKey, JSON.stringify(draft));
|
||||
} catch (error) {
|
||||
console.warn('Failed to persist capital CPP doc params draft:', error);
|
||||
}
|
||||
}
|
||||
+91
@@ -0,0 +1,91 @@
|
||||
<template lang="pug">
|
||||
.capital-council-group
|
||||
.capital-council-group__item(
|
||||
v-for='(step, index) in steps'
|
||||
:key='step.id'
|
||||
)
|
||||
.capital-council-group__head
|
||||
.capital-council-group__title
|
||||
span.capital-council-group__index {{ index + 1 }}.
|
||||
span {{ step.title }}
|
||||
.capital-council-group__status
|
||||
BaseChip(
|
||||
v-if='step.status === "completed"'
|
||||
variant='pos'
|
||||
size='sm'
|
||||
) Утверждено советом
|
||||
BaseChip(
|
||||
v-else-if='step.status === "in_progress"'
|
||||
variant='warn'
|
||||
size='sm'
|
||||
) Ожидаем решение совета
|
||||
|
||||
p.capital-council-group__desc.t-body2 {{ step.description }}
|
||||
|
||||
BaseButton.capital-council-group__action(
|
||||
v-if='canDeclare(step)'
|
||||
variant='primary'
|
||||
size='sm'
|
||||
:disable='busy'
|
||||
@click='emit("declare", step.id)'
|
||||
) Объявить собрание совета
|
||||
|
||||
q-separator.capital-council-group__sep(v-if='index < steps.length - 1')
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { BaseButton, BaseChip } from 'src/shared/ui/base';
|
||||
import type { ICouncilOnboardingStep } from 'src/shared/ui/CouncilOnboarding';
|
||||
|
||||
defineProps<{
|
||||
steps: ICouncilOnboardingStep[];
|
||||
busy?: boolean;
|
||||
canDeclare: (step: ICouncilOnboardingStep) => boolean;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
declare: [stepId: string];
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.capital-council-group__item {
|
||||
padding: var(--p-2) 0;
|
||||
}
|
||||
|
||||
.capital-council-group__head {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: var(--p-3);
|
||||
}
|
||||
|
||||
.capital-council-group__title {
|
||||
display: flex;
|
||||
gap: var(--p-2);
|
||||
font-weight: 600;
|
||||
color: var(--p-ink);
|
||||
}
|
||||
|
||||
.capital-council-group__index {
|
||||
flex: 0 0 auto;
|
||||
color: var(--p-ink-2);
|
||||
}
|
||||
|
||||
.capital-council-group__status {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.capital-council-group__desc {
|
||||
margin: var(--p-2) 0 0;
|
||||
color: var(--p-ink-2);
|
||||
}
|
||||
|
||||
.capital-council-group__action {
|
||||
margin-top: var(--p-3);
|
||||
}
|
||||
|
||||
.capital-council-group__sep {
|
||||
margin-top: var(--p-4);
|
||||
}
|
||||
</style>
|
||||
+443
-105
@@ -1,93 +1,124 @@
|
||||
<template lang="pug">
|
||||
div
|
||||
// Показываем лоадер пока данные загружаются
|
||||
div.capital-onboarding.q-pa-md
|
||||
WindowLoader(
|
||||
v-if="loading"
|
||||
text="Загрузка данных онбординга..."
|
||||
v-if='loading'
|
||||
text='Загрузка данных онбординга...'
|
||||
)
|
||||
// Показываем шаги если онбординг не завершен и данные загружены
|
||||
q-card(v-else-if="!isOnboardingCompleted" flat)
|
||||
q-card-section.row.items-center.justify-between
|
||||
div
|
||||
div.text-h5 Адаптация к работе с программой "Благорост"
|
||||
template(v-else-if='!isOnboardingCompleted')
|
||||
.capital-onboarding__shell
|
||||
BaseBanner.capital-onboarding__banner(variant='info')
|
||||
template(#icon)
|
||||
q-icon(name='info' size='20px')
|
||||
| Сначала заполните параметры положений «ГЕНЕРАТОР» и «БЛАГОРОСТ», затем объявите собрания
|
||||
| совета для утверждения документов ЦПП.
|
||||
|
||||
q-separator
|
||||
q-card-section
|
||||
q-list(separator)
|
||||
q-item(v-for="(step, index) in config?.steps || []" :key="step.id")
|
||||
q-item-section
|
||||
div.row.items-center.q-gutter-sm
|
||||
q-icon(
|
||||
:name="getIcon(step)"
|
||||
:color="getIconColor(step)"
|
||||
size="22px"
|
||||
)
|
||||
div.text-subtitle1 {{ index + 1 }}. {{ step.title }}
|
||||
div.text-caption.text-grey-7 {{ step.description }}
|
||||
q-chip(
|
||||
v-if="step.status === 'in_progress'"
|
||||
dense
|
||||
color="amber"
|
||||
text-color="black"
|
||||
icon="hourglass_top"
|
||||
class="q-mt-xs"
|
||||
) Ожидаем решение совета
|
||||
q-item-section(side)
|
||||
q-btn(
|
||||
v-if="showAction(index)"
|
||||
:disable="submitting || generatingDocument"
|
||||
:loading="generatingDocument && currentStepId === step.id"
|
||||
color="primary"
|
||||
label="Объявить собрание совета"
|
||||
@click="() => openDialog(step)"
|
||||
)
|
||||
|
||||
BaseDialog(
|
||||
v-model="dialogOpen",
|
||||
title="Предложение повестки",
|
||||
size="lg",
|
||||
:close-on-backdrop="false",
|
||||
:close-on-escape="false",
|
||||
@update:model-value="(v) => !v && closeDialog()"
|
||||
)
|
||||
Loader(
|
||||
v-if="generatingDocument"
|
||||
text="Генерация документа..."
|
||||
VerticalStepper.capital-onboarding__stepper(
|
||||
:steps='wizardSteps'
|
||||
:active-key='activeWizardStep'
|
||||
:completed='completedWizardSteps'
|
||||
:clickable-completed='true'
|
||||
@change='onStepperChange'
|
||||
)
|
||||
q-card-section(v-else-if="currentStep && currentGeneratedDoc && !generatingDocument")
|
||||
template(#active='{ step }')
|
||||
CapitalProgramDocumentStepPanel(
|
||||
v-if='isDocWizardStep(step.key)'
|
||||
v-model='form'
|
||||
:preview-html='getDocPreviewHtml(step.key)'
|
||||
:loading='isDocStepLoading(step.key)'
|
||||
@refresh='refreshDocStep(step.key)'
|
||||
)
|
||||
|
||||
CapitalCouncilStepGroup(
|
||||
v-else-if='isCouncilGroupStep(step.key)'
|
||||
:steps='getCouncilStepsForGroup(step.key)'
|
||||
:busy='submitting || generatingDocument'
|
||||
:can-declare='canDeclareCouncilStep'
|
||||
@declare='openCouncilDialog'
|
||||
)
|
||||
|
||||
.capital-onboarding__footer(v-if='showFooter')
|
||||
BaseButton(
|
||||
v-if='showBackButton'
|
||||
variant='ghost'
|
||||
:disable='footerBusy'
|
||||
@click='goBack'
|
||||
) Назад
|
||||
q-space(v-if='showBackButton && showContinueButton')
|
||||
BaseButton(
|
||||
v-if='showContinueButton'
|
||||
variant='primary'
|
||||
:class='{ "capital-onboarding__continue--solo": !showBackButton }'
|
||||
:loading='footerBusy'
|
||||
@click='goNext'
|
||||
) {{ continueLabel }}
|
||||
|
||||
BaseDialog(
|
||||
v-model='dialogOpen'
|
||||
title='Предложение повестки'
|
||||
size='lg'
|
||||
:close-on-backdrop='false'
|
||||
:close-on-escape='false'
|
||||
@update:model-value='(v) => !v && closeDialog()'
|
||||
)
|
||||
Loader(
|
||||
v-if='generatingDocument'
|
||||
text='Генерация документа...'
|
||||
)
|
||||
template(v-else-if='currentStep && currentGeneratedDoc && !generatingDocument')
|
||||
.capital-onboarding__dialog-section
|
||||
div.row.items-center.q-gutter-xs.text-subtitle1.text-weight-medium
|
||||
q-icon(name="help_outline" size="18px" class="text-primary")
|
||||
q-icon(name='help_outline' size='18px' color='primary')
|
||||
span Вопрос на повестке
|
||||
div.q-mt-sm.q-pa-sm.text-body1.rounded-borders {{ currentStep.question }}
|
||||
div.q-mt-sm.t-body {{ currentStep.question }}
|
||||
|
||||
q-separator.q-my-md
|
||||
|
||||
div.row.items-center.q-gutter-xs.text-subtitle1.text-weight-medium
|
||||
q-icon(name="gavel" size="18px" class="text-primary")
|
||||
q-icon(name='gavel' size='18px' color='primary')
|
||||
span Проект решения
|
||||
div.q-mt-sm.q-pa-sm.rounded-borders
|
||||
div(v-if="currentStep.decisionPrefix") {{ currentStep.decisionPrefix }}
|
||||
DocumentHtmlReader(:html="currentGeneratedDoc.html" :sanitize="false")
|
||||
div.q-mt-sm.text-caption.text-grey-6
|
||||
div.q-mt-sm
|
||||
div(v-if='currentStep.decisionPrefix') {{ currentStep.decisionPrefix }}
|
||||
DocumentHtmlReader(:html='currentGeneratedDoc.html' :sanitize='false')
|
||||
div.t-caption.text-grey-6.q-mt-sm
|
||||
strong {{ currentGeneratedDoc.full_title }}
|
||||
|
||||
div.q-pb-lg(v-if="currentGeneratedDoc && !generatingDocument")
|
||||
q-btn(flat label="Отмена" @click="closeDialog" :disable="submitting")
|
||||
q-btn(
|
||||
color="primary"
|
||||
label="Объявить"
|
||||
:loading="submitting"
|
||||
@click="submitStep"
|
||||
)
|
||||
template(#footer v-if='currentGeneratedDoc && !generatingDocument')
|
||||
BaseButton(variant='ghost' :disable='submitting' @click='closeDialog') Отмена
|
||||
BaseButton(variant='primary' :loading='submitting' @click='submitStep') Объявить
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { computed, ref, watch, onMounted, onBeforeUnmount } from 'vue';
|
||||
import { useQuasar } from 'quasar';
|
||||
import { DocumentHtmlReader } from 'src/shared/ui/DocumentHtmlReader';
|
||||
import { BaseBanner, BaseButton } from 'src/shared/ui/base';
|
||||
import { BaseDialog } from 'src/shared/ui/base/BaseDialog';
|
||||
import { WindowLoader, Loader } from 'src/shared/ui/Loader';
|
||||
import { VerticalStepper } from 'src/shared/ui/domain/VerticalStepper';
|
||||
import type { StepperStep } from 'src/shared/ui/domain/VerticalStepper';
|
||||
import type { ICouncilOnboardingStep } from 'src/shared/ui/CouncilOnboarding';
|
||||
import { useDataPoller } from 'src/shared/lib/composables';
|
||||
import { POLL_INTERVALS } from 'src/shared/lib/consts';
|
||||
import CapitalProgramDocumentStepPanel from './CapitalProgramDocumentStepPanel.vue';
|
||||
import CapitalCouncilStepGroup from './CapitalCouncilStepGroup.vue';
|
||||
import { useCapitalOnboarding } from '../model';
|
||||
import { useCapitalProgramDocParams, getRegistryIdForDocWizardStep } from '../model/useCapitalProgramDocParams';
|
||||
import {
|
||||
COUNCIL_GROUP_FOUNDATION,
|
||||
COUNCIL_GROUP_OFFERS,
|
||||
COUNCIL_STEP_GROUP_ORDER,
|
||||
COUNCIL_WIZARD_STEP_META,
|
||||
DOC_WIZARD_STEP_BLAGOROST,
|
||||
DOC_WIZARD_STEP_GENERATOR,
|
||||
DOC_WIZARD_STEP_ORDER,
|
||||
getCouncilStepIdsForGroup,
|
||||
isCouncilGroupStep,
|
||||
isDocWizardStep,
|
||||
normalizeWizardStepKey,
|
||||
type CouncilGroupStepKey,
|
||||
} from '../model/capitalOnboardingWizard';
|
||||
|
||||
const $q = useQuasar();
|
||||
|
||||
const {
|
||||
config,
|
||||
@@ -96,67 +127,374 @@ const {
|
||||
generatingDocument,
|
||||
currentGeneratedDoc,
|
||||
isOnboardingCompleted,
|
||||
hasPendingCouncilDecisions,
|
||||
isDocParamsReady,
|
||||
loadState,
|
||||
refreshState,
|
||||
handleStepClick,
|
||||
handleStepSubmit,
|
||||
handleDocParamsSaved,
|
||||
} = useCapitalOnboarding();
|
||||
|
||||
const {
|
||||
form,
|
||||
previews,
|
||||
saving,
|
||||
loadingRegistryId,
|
||||
wizardStepKey,
|
||||
restoreDraftFromStorage,
|
||||
persistDraftToStorage,
|
||||
validateSection,
|
||||
loadPreview,
|
||||
ensurePreview,
|
||||
saveParams,
|
||||
setWizardStepKey,
|
||||
} = useCapitalProgramDocParams({
|
||||
onSaved: () => handleDocParamsSaved(),
|
||||
});
|
||||
|
||||
const activeWizardStep = wizardStepKey;
|
||||
const advancing = ref(false);
|
||||
const dialogOpen = ref(false);
|
||||
const currentStep = ref<ICouncilOnboardingStep | null>(null);
|
||||
const currentStepId = ref<string | null>(null);
|
||||
|
||||
const councilSteps = computed(() => config.value?.steps ?? []);
|
||||
|
||||
const getIcon = (step: ICouncilOnboardingStep) => {
|
||||
if (step.status === 'completed') return 'task_alt';
|
||||
if (step.status === 'in_progress') return 'hourglass_top';
|
||||
return 'radio_button_unchecked';
|
||||
};
|
||||
const wizardSteps = computed<StepperStep[]>(() => [
|
||||
{
|
||||
key: DOC_WIZARD_STEP_GENERATOR,
|
||||
label: 'Параметры «ГЕНЕРАТОР»',
|
||||
description: 'Переменные положения о целевой программе «ГЕНЕРАТОР»',
|
||||
},
|
||||
{
|
||||
key: DOC_WIZARD_STEP_BLAGOROST,
|
||||
label: 'Параметры «БЛАГОРОСТ»',
|
||||
description: 'Переменные положения о целевой программе «БЛАГОРОСТ»',
|
||||
},
|
||||
...COUNCIL_STEP_GROUP_ORDER.map((groupKey) => ({
|
||||
key: groupKey,
|
||||
label: COUNCIL_WIZARD_STEP_META[groupKey].label,
|
||||
description: COUNCIL_WIZARD_STEP_META[groupKey].description,
|
||||
disabled: isCouncilGroupDisabled(groupKey),
|
||||
})),
|
||||
]);
|
||||
|
||||
const getIconColor = (step: ICouncilOnboardingStep) => {
|
||||
if (step.status === 'completed') return 'green-6';
|
||||
if (step.status === 'in_progress') return 'orange-6';
|
||||
return 'grey-6';
|
||||
};
|
||||
const wizardStepOrder = computed<string[]>(() => [
|
||||
...DOC_WIZARD_STEP_ORDER,
|
||||
...COUNCIL_STEP_GROUP_ORDER,
|
||||
]);
|
||||
|
||||
const isDependenciesCompleted = (step: ICouncilOnboardingStep) => {
|
||||
if (!step.depends_on || step.depends_on.length === 0) return true;
|
||||
if (!config.value?.steps) return true;
|
||||
const completedWizardSteps = computed(() => {
|
||||
const completed: string[] = [];
|
||||
const activeIndex = wizardStepOrder.value.indexOf(activeWizardStep.value);
|
||||
|
||||
return step.depends_on.every(depId => {
|
||||
const depStep = config.value.steps.find(s => s.id === depId);
|
||||
return depStep && depStep.status === 'completed';
|
||||
});
|
||||
};
|
||||
if (activeIndex > 0 || isDocParamsReady.value) {
|
||||
completed.push(DOC_WIZARD_STEP_GENERATOR);
|
||||
}
|
||||
if (isDocParamsReady.value) {
|
||||
completed.push(DOC_WIZARD_STEP_BLAGOROST);
|
||||
}
|
||||
|
||||
const showAction = (index: number) => {
|
||||
if (!config.value?.steps) return false;
|
||||
const step = config.value.steps[index];
|
||||
if (!step) return false;
|
||||
for (const groupKey of COUNCIL_STEP_GROUP_ORDER) {
|
||||
if (isCouncilGroupCompleted(groupKey)) {
|
||||
completed.push(groupKey);
|
||||
}
|
||||
}
|
||||
|
||||
return completed;
|
||||
});
|
||||
|
||||
const footerBusy = computed(() => saving.value || advancing.value || !!loadingRegistryId.value);
|
||||
|
||||
const showBackButton = computed(() => {
|
||||
const index = wizardStepOrder.value.indexOf(activeWizardStep.value);
|
||||
return index > 0;
|
||||
});
|
||||
|
||||
const showContinueButton = computed(() => isDocWizardStep(activeWizardStep.value));
|
||||
|
||||
const showFooter = computed(() => showBackButton.value || showContinueButton.value);
|
||||
|
||||
const continueLabel = computed(() =>
|
||||
activeWizardStep.value === DOC_WIZARD_STEP_BLAGOROST ? 'Сохранить и продолжить' : 'Продолжить',
|
||||
);
|
||||
|
||||
function getCouncilStep(stepId: string): ICouncilOnboardingStep | undefined {
|
||||
return councilSteps.value.find((step) => step.id === stepId);
|
||||
}
|
||||
|
||||
function getCouncilStepsForGroup(groupKey: CouncilGroupStepKey): ICouncilOnboardingStep[] {
|
||||
return getCouncilStepIdsForGroup(groupKey)
|
||||
.map((stepId) => getCouncilStep(stepId))
|
||||
.filter((step): step is ICouncilOnboardingStep => step != null);
|
||||
}
|
||||
|
||||
function isCouncilGroupCompleted(groupKey: CouncilGroupStepKey): boolean {
|
||||
return getCouncilStepsForGroup(groupKey).every((step) => step.status === 'completed');
|
||||
}
|
||||
|
||||
function isCouncilGroupDisabled(groupKey: CouncilGroupStepKey): boolean {
|
||||
if (groupKey === COUNCIL_GROUP_FOUNDATION) {
|
||||
return !isDocParamsReady.value;
|
||||
}
|
||||
if (groupKey === COUNCIL_GROUP_OFFERS) {
|
||||
return getCouncilStepsForGroup(groupKey).every(
|
||||
(step) =>
|
||||
step.status !== 'completed' &&
|
||||
step.status !== 'in_progress' &&
|
||||
!canDeclareCouncilStep(step),
|
||||
);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function getFirstAvailableCouncilGroup(): CouncilGroupStepKey | null {
|
||||
for (const groupKey of COUNCIL_STEP_GROUP_ORDER) {
|
||||
if (!isCouncilGroupCompleted(groupKey) && !isCouncilGroupDisabled(groupKey)) {
|
||||
return groupKey;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function syncWizardStepWithCouncilProgress() {
|
||||
if (!isDocParamsReady.value || isOnboardingCompleted.value) return;
|
||||
|
||||
const firstAvailable = getFirstAvailableCouncilGroup();
|
||||
if (!firstAvailable) return;
|
||||
|
||||
const current = activeWizardStep.value;
|
||||
|
||||
if (isDocWizardStep(current)) {
|
||||
setWizardStepKey(firstAvailable);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isCouncilGroupStep(current)) return;
|
||||
|
||||
if (isCouncilGroupCompleted(current) && firstAvailable !== current) {
|
||||
setWizardStepKey(firstAvailable);
|
||||
}
|
||||
}
|
||||
|
||||
function getDocPreviewHtml(stepKey: string): string {
|
||||
const registryId = getRegistryIdForDocWizardStep(stepKey);
|
||||
return registryId ? previews[registryId]?.html ?? '' : '';
|
||||
}
|
||||
|
||||
function isDocStepLoading(stepKey: string): boolean {
|
||||
const registryId = getRegistryIdForDocWizardStep(stepKey);
|
||||
return registryId != null && loadingRegistryId.value === registryId;
|
||||
}
|
||||
|
||||
async function refreshDocStep(stepKey: string) {
|
||||
const registryId = getRegistryIdForDocWizardStep(stepKey);
|
||||
if (registryId == null) return;
|
||||
await loadPreview(registryId);
|
||||
}
|
||||
|
||||
function canDeclareCouncilStep(step: ICouncilOnboardingStep): boolean {
|
||||
if (step.status === 'completed' || step.status === 'in_progress') return false;
|
||||
return isDependenciesCompleted(step);
|
||||
};
|
||||
if (!step.depends_on?.length) return true;
|
||||
return step.depends_on.every((depId) => getCouncilStep(depId)?.status === 'completed');
|
||||
}
|
||||
|
||||
function onStepperChange(key: string) {
|
||||
if (isDocWizardStep(key)) {
|
||||
setWizardStepKey(key);
|
||||
return;
|
||||
}
|
||||
if (isCouncilGroupStep(key) && !isCouncilGroupDisabled(key)) {
|
||||
setWizardStepKey(key);
|
||||
}
|
||||
}
|
||||
|
||||
async function goNext() {
|
||||
if (activeWizardStep.value === DOC_WIZARD_STEP_GENERATOR) {
|
||||
const validationError = validateSection(994);
|
||||
if (validationError) {
|
||||
$q.notify({ type: 'warning', message: validationError });
|
||||
return;
|
||||
}
|
||||
|
||||
advancing.value = true;
|
||||
try {
|
||||
await ensurePreview(994);
|
||||
setWizardStepKey(DOC_WIZARD_STEP_BLAGOROST);
|
||||
} finally {
|
||||
advancing.value = false;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (activeWizardStep.value === DOC_WIZARD_STEP_BLAGOROST) {
|
||||
const validationError = validateSection(998);
|
||||
if (validationError) {
|
||||
$q.notify({ type: 'warning', message: validationError });
|
||||
return;
|
||||
}
|
||||
|
||||
advancing.value = true;
|
||||
try {
|
||||
const saved = await saveParams();
|
||||
if (!saved) return;
|
||||
|
||||
const firstCouncilGroup = getFirstAvailableCouncilGroup();
|
||||
if (firstCouncilGroup) {
|
||||
setWizardStepKey(firstCouncilGroup);
|
||||
}
|
||||
} finally {
|
||||
advancing.value = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function goBack() {
|
||||
const index = wizardStepOrder.value.indexOf(activeWizardStep.value);
|
||||
if (index <= 0) return;
|
||||
setWizardStepKey(wizardStepOrder.value[index - 1]);
|
||||
}
|
||||
|
||||
async function openCouncilDialog(stepId: string) {
|
||||
const step = getCouncilStep(stepId);
|
||||
if (!step || !canDeclareCouncilStep(step)) return;
|
||||
|
||||
const openDialog = async (step: ICouncilOnboardingStep) => {
|
||||
currentStep.value = step;
|
||||
currentStepId.value = step.id;
|
||||
dialogOpen.value = true;
|
||||
|
||||
// Генерируем документ
|
||||
await handleStepClick(step);
|
||||
};
|
||||
}
|
||||
|
||||
const closeDialog = () => {
|
||||
function closeDialog() {
|
||||
dialogOpen.value = false;
|
||||
currentStep.value = null;
|
||||
currentStepId.value = null;
|
||||
};
|
||||
}
|
||||
|
||||
const submitStep = async () => {
|
||||
async function submitStep() {
|
||||
if (!currentStep.value) return;
|
||||
|
||||
await handleStepSubmit(currentStep.value);
|
||||
closeDialog();
|
||||
};
|
||||
syncWizardStepWithCouncilProgress();
|
||||
}
|
||||
|
||||
onMounted(loadState);
|
||||
const shouldPollCouncilState = computed(
|
||||
() => hasPendingCouncilDecisions.value && !isOnboardingCompleted.value,
|
||||
);
|
||||
|
||||
const { start: startCouncilPoll, stop: stopCouncilPoll } = useDataPoller(
|
||||
async () => {
|
||||
await refreshState();
|
||||
syncWizardStepWithCouncilProgress();
|
||||
},
|
||||
{
|
||||
interval: POLL_INTERVALS.FAST,
|
||||
immediate: false,
|
||||
enabled: shouldPollCouncilState,
|
||||
},
|
||||
);
|
||||
|
||||
function handleVisibilityChange() {
|
||||
if (document.hidden || !shouldPollCouncilState.value) return;
|
||||
void refreshState().then(() => syncWizardStepWithCouncilProgress());
|
||||
}
|
||||
|
||||
watch(shouldPollCouncilState, (enabled) => {
|
||||
if (enabled) {
|
||||
startCouncilPoll();
|
||||
} else {
|
||||
stopCouncilPoll();
|
||||
}
|
||||
});
|
||||
|
||||
watch(
|
||||
() => councilSteps.value.map((step) => `${step.id}:${step.status}`).join('|'),
|
||||
() => {
|
||||
syncWizardStepWithCouncilProgress();
|
||||
},
|
||||
);
|
||||
|
||||
watch(
|
||||
() => activeWizardStep.value,
|
||||
(stepKey) => {
|
||||
if (!isDocWizardStep(stepKey)) return;
|
||||
const registryId = getRegistryIdForDocWizardStep(stepKey);
|
||||
if (registryId != null) {
|
||||
void ensurePreview(registryId);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
watch(
|
||||
form,
|
||||
() => {
|
||||
persistDraftToStorage();
|
||||
},
|
||||
{ deep: true },
|
||||
);
|
||||
|
||||
onMounted(async () => {
|
||||
await loadState();
|
||||
restoreDraftFromStorage();
|
||||
setWizardStepKey(normalizeWizardStepKey(activeWizardStep.value));
|
||||
syncWizardStepWithCouncilProgress();
|
||||
|
||||
if (isDocWizardStep(activeWizardStep.value)) {
|
||||
const registryId = getRegistryIdForDocWizardStep(activeWizardStep.value);
|
||||
if (registryId != null) {
|
||||
void ensurePreview(registryId);
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldPollCouncilState.value) {
|
||||
startCouncilPoll();
|
||||
}
|
||||
|
||||
document.addEventListener('visibilitychange', handleVisibilityChange);
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
stopCouncilPoll();
|
||||
document.removeEventListener('visibilitychange', handleVisibilityChange);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.capital-onboarding {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
.capital-onboarding__shell {
|
||||
width: 100%;
|
||||
max-width: 920px;
|
||||
}
|
||||
|
||||
.capital-onboarding__banner {
|
||||
margin-bottom: var(--p-4);
|
||||
}
|
||||
|
||||
.capital-onboarding__stepper {
|
||||
margin-top: var(--p-2);
|
||||
}
|
||||
|
||||
.capital-onboarding__continue--solo {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.capital-onboarding__dialog-section {
|
||||
padding: var(--p-1) 0;
|
||||
}
|
||||
|
||||
.capital-onboarding__footer {
|
||||
position: sticky;
|
||||
bottom: 0;
|
||||
z-index: 2;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: var(--p-3);
|
||||
margin-top: var(--p-5);
|
||||
padding: var(--p-4) 0;
|
||||
background: var(--p-canvas);
|
||||
border-top: 1px solid var(--p-line);
|
||||
}
|
||||
</style>
|
||||
|
||||
+243
-329
@@ -1,387 +1,301 @@
|
||||
<template lang="pug">
|
||||
BaseCard(title='Параметры документов ЦПП')
|
||||
p.capital-doc-params__intro.t-body2
|
||||
| Заполните поля, которые будут подставлены в положения и оферты программ «ГЕНЕРАТОР» и «БЛАГОРОСТ».
|
||||
| Перед установкой расширения сформируйте предпросмотр документов: так параметры сохранятся в PrivateData, а в config попадёт только hash.
|
||||
BaseCard.capital-doc-params(title='Параметры документов ЦПП')
|
||||
p.t-body-sm.capital-doc-params__intro
|
||||
| Заполните параметры положений «ГЕНЕРАТОР» и «БЛАГОРОСТ» прямо в тексте документа.
|
||||
|
||||
BaseInput(
|
||||
:model-value='config?.capital_program_doc_data_hash || ""'
|
||||
label='Hash PrivateData документов'
|
||||
hint='Появится после формирования предпросмотра'
|
||||
readonly
|
||||
mono
|
||||
)
|
||||
q-tabs(v-model='activeTab' dense align='left' class='capital-doc-params__tabs')
|
||||
q-tab(
|
||||
v-for='section in DOCUMENT_SECTIONS'
|
||||
:key='section.registryId'
|
||||
:name='section.registryId'
|
||||
:label='section.tabLabel'
|
||||
)
|
||||
q-badge(v-if='getMissingCount(section.registryId)' color='warning' floating) {{ getMissingCount(section.registryId) }}
|
||||
|
||||
BaseBanner(v-if='config?.capital_program_doc_data_hash' variant='pos')
|
||||
| Параметры сохранены. Hash: {{ config.capital_program_doc_data_hash }}
|
||||
|
||||
q-expansion-item(default-opened label='Протокол и кооператив' icon='description')
|
||||
.row.q-col-gutter-md.q-mt-sm
|
||||
.col-12.col-md-3(v-for='field in protocolFields' :key='field.key')
|
||||
BaseInput(
|
||||
v-model='form[field.key]'
|
||||
:label='field.label'
|
||||
:error='fieldErrors[field.key]'
|
||||
)
|
||||
.col-12.col-md-6(v-for='field in cooperativeFields' :key='field.key')
|
||||
BaseInput(
|
||||
v-model='form[field.key]'
|
||||
:label='field.label'
|
||||
:error='fieldErrors[field.key]'
|
||||
q-tab-panels(v-model='activeTab' animated class='capital-doc-params__panels')
|
||||
q-tab-panel(
|
||||
v-for='section in DOCUMENT_SECTIONS'
|
||||
:key='section.registryId'
|
||||
:name='section.registryId'
|
||||
)
|
||||
.capital-doc-params__toolbar
|
||||
BaseButton(
|
||||
color='primary'
|
||||
outline
|
||||
:loading='loadingRegistryId === section.registryId'
|
||||
:disable='!!loadingRegistryId'
|
||||
@click='loadPreview(section.registryId)'
|
||||
)
|
||||
q-icon(name='refresh' class='q-mr-xs')
|
||||
| {{ previews[section.registryId] ? 'Обновить предпросмотр' : 'Сформировать предпросмотр' }}
|
||||
|
||||
q-expansion-item(label='Программа ГЕНЕРАТОР' icon='bolt')
|
||||
.capital-doc-params__section
|
||||
BaseInput(
|
||||
v-for='field in generatorFields'
|
||||
:key='field.key'
|
||||
v-model='form[field.key]'
|
||||
:label='field.label'
|
||||
:type='field.type || "textarea"'
|
||||
:autogrow='field.type !== "text"'
|
||||
:error='fieldErrors[field.key]'
|
||||
q-inner-loading(:showing='loadingRegistryId === section.registryId')
|
||||
|
||||
CapitalProgramInlineDocumentPreview(
|
||||
v-if='previews[section.registryId]'
|
||||
v-model='form'
|
||||
:html='getPreviewHtml(section.registryId)'
|
||||
:field-labels='FIELD_LABELS'
|
||||
class='q-mt-md'
|
||||
)
|
||||
|
||||
q-expansion-item(label='Программа БЛАГОРОСТ' icon='eco')
|
||||
.capital-doc-params__section
|
||||
BaseInput(
|
||||
v-for='field in blagorostFields'
|
||||
:key='field.key'
|
||||
v-model='form[field.key]'
|
||||
:label='field.label'
|
||||
type='textarea'
|
||||
autogrow
|
||||
:error='fieldErrors[field.key]'
|
||||
)
|
||||
|
||||
q-expansion-item(label='Возврат, источники и оферты' icon='article')
|
||||
.capital-doc-params__section
|
||||
BaseInput(
|
||||
v-for='field in returnFields'
|
||||
:key='field.key'
|
||||
v-model='form[field.key]'
|
||||
:label='field.label'
|
||||
:type='field.type || "textarea"'
|
||||
:autogrow='field.type !== "text"'
|
||||
:error='fieldErrors[field.key]'
|
||||
)
|
||||
p.t-caption.text-grey-6.q-mt-md(v-else)
|
||||
| Нажмите «Сформировать предпросмотр», чтобы увидеть документ с редактируемыми полями.
|
||||
|
||||
.capital-doc-params__footer
|
||||
p.capital-doc-params__hint.t-caption
|
||||
| Предпросмотр генерирует документы без публикации и сохраняет PrivateData по hash.
|
||||
BaseButton(:loading='isGenerating' @click='generatePreview')
|
||||
| Сформировать предпросмотр
|
||||
|
||||
BaseDialog(v-model='previewOpen' title='Предпросмотр положений ЦПП' size='xl')
|
||||
.capital-doc-params__preview(v-if='previewDocuments.length')
|
||||
q-tabs(v-model='activePreviewTab' dense align='left' class='capital-doc-params__tabs')
|
||||
q-tab(
|
||||
v-for='doc in previewDocuments'
|
||||
:key='doc.registry_id'
|
||||
:name='String(doc.registry_id)'
|
||||
:label='doc.title'
|
||||
)
|
||||
q-separator
|
||||
q-tab-panels(v-model='activePreviewTab' animated)
|
||||
q-tab-panel(
|
||||
v-for='doc in previewDocuments'
|
||||
:key='doc.registry_id'
|
||||
:name='String(doc.registry_id)'
|
||||
)
|
||||
.doc-preview
|
||||
DocumentHtmlReader(:html='doc.html' :sanitize='false')
|
||||
template(#footer)
|
||||
BaseButton(variant='ghost' @click='previewOpen = false')
|
||||
| Закрыть
|
||||
BaseButton(
|
||||
color='primary'
|
||||
:loading='saving'
|
||||
:disable='!!loadingRegistryId'
|
||||
@click='saveParams'
|
||||
)
|
||||
| Сохранить параметры
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, reactive, ref, watch } from 'vue';
|
||||
import { Mutations } from '@coopenomics/sdk';
|
||||
import { computed, onMounted, reactive, ref, watch } from 'vue';
|
||||
import { useQuasar } from 'quasar';
|
||||
import type { Cooperative } from 'cooptypes';
|
||||
import { useSystemStore } from 'src/entities/System/model';
|
||||
import { useSessionStore } from 'src/entities/Session';
|
||||
import { client } from 'src/shared/api/client';
|
||||
import { FailAlert, SuccessAlert } from 'src/shared/api';
|
||||
import { BaseBanner, BaseButton, BaseCard, BaseDialog, BaseInput } from 'src/shared/ui/base';
|
||||
import { DocumentHtmlReader } from 'src/shared/ui/DocumentHtmlReader';
|
||||
import { BaseButton, BaseCard } from 'src/shared/ui/base';
|
||||
import { api as documentApi } from 'src/shared/lib/document/api';
|
||||
import { api } from '../api';
|
||||
import CapitalProgramInlineDocumentPreview from './CapitalProgramInlineDocumentPreview.vue';
|
||||
import {
|
||||
ALL_DOC_FIELDS,
|
||||
BLAGOROST_DOC_FIELDS,
|
||||
DOCUMENT_SECTIONS,
|
||||
FIELD_LABELS,
|
||||
GENERATOR_DOC_FIELDS,
|
||||
PREVIEW_PLACEHOLDER,
|
||||
type EditableFieldKey,
|
||||
} from '../model/capitalProgramDocFields';
|
||||
import {
|
||||
getCapitalProgramDocParamsDraftKey,
|
||||
readCapitalProgramDocParamsDraft,
|
||||
writeCapitalProgramDocParamsDraft,
|
||||
} from '../model/useCapitalProgramDocParamsDraft';
|
||||
|
||||
type CapitalProgramPrivateData = {
|
||||
approval_protocol_number: string;
|
||||
approval_protocol_day: string;
|
||||
approval_protocol_month: string;
|
||||
approval_protocol_year: string;
|
||||
cooperative_name: string;
|
||||
cooperative_short_name: string;
|
||||
cooperative_quoted_name: string;
|
||||
website: string;
|
||||
chairman_full_name: string;
|
||||
generator_program_purpose: string;
|
||||
eoap_definition: string;
|
||||
generator_task_goal: string;
|
||||
idea_unit_cost: string;
|
||||
idea_unit_cost_words: string;
|
||||
blagorost_goal_expansion: string;
|
||||
blagorost_task_expansion: string;
|
||||
blagorost_task_development: string;
|
||||
return_source_description: string;
|
||||
return_additional_source: string;
|
||||
offer_template_number: string;
|
||||
};
|
||||
|
||||
type FieldDefinition = {
|
||||
key: keyof CapitalProgramPrivateData;
|
||||
label: string;
|
||||
type?: 'text' | 'textarea';
|
||||
};
|
||||
|
||||
type CapitalProgramConfig = Record<string, unknown> & {
|
||||
capital_program_doc_data_hash?: string | null;
|
||||
};
|
||||
|
||||
type GeneratedPreviewDocument = NonNullable<
|
||||
Mutations.Documents.GenerateDocument.IOutput[typeof Mutations.Documents.GenerateDocument.name]
|
||||
>;
|
||||
|
||||
interface Props {
|
||||
config: CapitalProgramConfig;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
const emit = defineEmits<{
|
||||
(event: 'update:config', value: CapitalProgramConfig): void;
|
||||
saved: [hash: string];
|
||||
}>();
|
||||
|
||||
const system = useSystemStore();
|
||||
const session = useSessionStore();
|
||||
const isGenerating = ref(false);
|
||||
const previewOpen = ref(false);
|
||||
const activePreviewTab = ref('994');
|
||||
const previewDocuments = ref<Array<{ registry_id: number; title: string; html: string; hash: string }>>([]);
|
||||
const fieldErrors = reactive<Partial<Record<keyof CapitalProgramPrivateData, string>>>({});
|
||||
const $q = useQuasar();
|
||||
const systemStore = useSystemStore();
|
||||
const sessionStore = useSessionStore();
|
||||
|
||||
const form = reactive<Omit<CapitalProgramPrivateData, 'cooperative_quoted_name'>>({
|
||||
approval_protocol_number: '',
|
||||
approval_protocol_day: '',
|
||||
approval_protocol_month: '',
|
||||
approval_protocol_year: '',
|
||||
cooperative_name: '',
|
||||
cooperative_short_name: '',
|
||||
website: '',
|
||||
chairman_full_name: '',
|
||||
generator_program_purpose: '',
|
||||
eoap_definition: '',
|
||||
generator_task_goal: '',
|
||||
idea_unit_cost: '',
|
||||
idea_unit_cost_words: '',
|
||||
blagorost_goal_expansion: '',
|
||||
blagorost_task_expansion: '',
|
||||
blagorost_task_development: '',
|
||||
return_source_description: '',
|
||||
return_additional_source: '',
|
||||
offer_template_number: '______',
|
||||
});
|
||||
const activeTab = ref<number>(994);
|
||||
const saving = ref(false);
|
||||
const savedHash = ref('');
|
||||
const loadingRegistryId = ref<number | null>(null);
|
||||
|
||||
const protocolFields: FieldDefinition[] = [
|
||||
{ key: 'approval_protocol_number', label: 'Номер протокола' },
|
||||
{ key: 'approval_protocol_day', label: 'День протокола' },
|
||||
{ key: 'approval_protocol_month', label: 'Месяц протокола' },
|
||||
{ key: 'approval_protocol_year', label: 'Год протокола' },
|
||||
];
|
||||
type PreviewState = {
|
||||
html: string;
|
||||
docDataHash: string;
|
||||
};
|
||||
|
||||
const cooperativeFields: FieldDefinition[] = [
|
||||
{ key: 'cooperative_name', label: 'Наименование кооператива' },
|
||||
{ key: 'cooperative_short_name', label: 'Краткое наименование (ПК)' },
|
||||
{ key: 'website', label: 'Сайт' },
|
||||
{ key: 'chairman_full_name', label: 'ФИО председателя' },
|
||||
];
|
||||
const previews = reactive<Partial<Record<number, PreviewState>>>({});
|
||||
|
||||
const generatorFields: FieldDefinition[] = [
|
||||
{ key: 'generator_program_purpose', label: 'Назначение программы ГЕНЕРАТОР' },
|
||||
{ key: 'eoap_definition', label: 'Определение ЕОАП' },
|
||||
{ key: 'generator_task_goal', label: 'Цель задач ГЕНЕРАТОР' },
|
||||
{ key: 'idea_unit_cost', label: 'Стоимость единицы идеи', type: 'text' },
|
||||
{ key: 'idea_unit_cost_words', label: 'Стоимость единицы идеи прописью', type: 'text' },
|
||||
];
|
||||
const DOC_FIELDS_BY_REGISTRY: Record<number, EditableFieldKey[]> = {
|
||||
994: GENERATOR_DOC_FIELDS,
|
||||
998: BLAGOROST_DOC_FIELDS,
|
||||
};
|
||||
|
||||
const blagorostFields: FieldDefinition[] = [
|
||||
{ key: 'blagorost_goal_expansion', label: 'Расширение цели БЛАГОРОСТ' },
|
||||
{ key: 'blagorost_task_expansion', label: 'Задача расширения БЛАГОРОСТ' },
|
||||
{ key: 'blagorost_task_development', label: 'Задача развития БЛАГОРОСТ' },
|
||||
];
|
||||
function createEmptyForm(): Record<EditableFieldKey, string> {
|
||||
return Object.fromEntries(ALL_DOC_FIELDS.map((key) => [key, ''])) as Record<EditableFieldKey, string>;
|
||||
}
|
||||
|
||||
const returnFields: FieldDefinition[] = [
|
||||
{ key: 'return_source_description', label: 'Источник возврата' },
|
||||
{ key: 'return_additional_source', label: 'Дополнительный источник возврата' },
|
||||
{ key: 'offer_template_number', label: 'Номер шаблона оферты', type: 'text' },
|
||||
];
|
||||
const form = reactive<Record<EditableFieldKey, string>>(createEmptyForm());
|
||||
|
||||
const allFields = computed(() => [
|
||||
...protocolFields,
|
||||
...cooperativeFields,
|
||||
...generatorFields,
|
||||
...blagorostFields,
|
||||
...returnFields,
|
||||
]);
|
||||
|
||||
watch(
|
||||
() => system.info,
|
||||
(info) => {
|
||||
if (!info) return;
|
||||
form.cooperative_name ||= String(info.coopname || '').toUpperCase();
|
||||
form.cooperative_short_name ||= 'ПК';
|
||||
form.website ||= info.website || info.contacts?.website || '';
|
||||
form.chairman_full_name ||= info.chairman?.full_name || info.chairman_full_name || '';
|
||||
},
|
||||
{ immediate: true }
|
||||
const draftStorageKey = computed(() =>
|
||||
getCapitalProgramDocParamsDraftKey(systemStore.info?.coopname ?? '', sessionStore.username ?? ''),
|
||||
);
|
||||
|
||||
function getPayload(): CapitalProgramPrivateData {
|
||||
const cooperativeName = String(form.cooperative_name || '').trim();
|
||||
return {
|
||||
...Object.fromEntries(
|
||||
allFields.value.map(({ key }) => [key, String(form[key] || '').trim()])
|
||||
) as Omit<CapitalProgramPrivateData, 'cooperative_quoted_name'>,
|
||||
cooperative_quoted_name: cooperativeName ? `«${cooperativeName}»` : '',
|
||||
};
|
||||
function restoreDraftFromStorage() {
|
||||
const draft = readCapitalProgramDocParamsDraft(draftStorageKey.value);
|
||||
if (!draft) return;
|
||||
|
||||
Object.assign(form, draft.form);
|
||||
savedHash.value = draft.savedHash;
|
||||
Object.assign(previews, draft.previews);
|
||||
activeTab.value = draft.activeTab ?? 994;
|
||||
}
|
||||
|
||||
function validatePayload(payload: CapitalProgramPrivateData): string[] {
|
||||
return allFields.value
|
||||
.filter(({ key }) => !payload[key])
|
||||
function persistDraftToStorage() {
|
||||
writeCapitalProgramDocParamsDraft(draftStorageKey.value, {
|
||||
form: { ...form },
|
||||
activeTab: activeTab.value,
|
||||
savedHash: savedHash.value,
|
||||
previews: { ...previews },
|
||||
});
|
||||
}
|
||||
|
||||
const editableFields = computed(() =>
|
||||
ALL_DOC_FIELDS.map((key) => ({
|
||||
key,
|
||||
label: FIELD_LABELS[key],
|
||||
})),
|
||||
);
|
||||
|
||||
function getPreviewHtml(registryId: number): string {
|
||||
return previews[registryId]?.html ?? '';
|
||||
}
|
||||
|
||||
function getMissingCount(registryId: number): number {
|
||||
const fields = DOC_FIELDS_BY_REGISTRY[registryId] ?? [];
|
||||
return fields.filter((key) => !String(form[key] ?? '').trim()).length;
|
||||
}
|
||||
|
||||
function getPreviewPayload(): Cooperative.Registry.CapitalProgramPrivateData {
|
||||
return Object.fromEntries(
|
||||
ALL_DOC_FIELDS.map((key) => [key, String(form[key] ?? '').trim() || PREVIEW_PLACEHOLDER]),
|
||||
) as Record<EditableFieldKey, string>;
|
||||
}
|
||||
|
||||
function getDocDataHashFromResult(result: Awaited<ReturnType<typeof documentApi.generateDocument>>): string {
|
||||
const meta = result.meta as { doc_data_hash?: string } | undefined;
|
||||
return meta?.doc_data_hash ?? '';
|
||||
}
|
||||
|
||||
function validatePayload(): string | null {
|
||||
const missing = editableFields.value
|
||||
.filter(({ key }) => !String(form[key] ?? '').trim())
|
||||
.map(({ label }) => label);
|
||||
}
|
||||
|
||||
function clearFieldErrors(): void {
|
||||
for (const key of Object.keys(fieldErrors) as Array<keyof CapitalProgramPrivateData>) {
|
||||
delete fieldErrors[key];
|
||||
}
|
||||
}
|
||||
|
||||
function setFieldErrors(missingLabels: string[]): void {
|
||||
clearFieldErrors();
|
||||
for (const { key, label } of allFields.value) {
|
||||
if (missingLabels.includes(label)) {
|
||||
fieldErrors[key] = 'Заполните поле';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === 'object' && value !== null;
|
||||
}
|
||||
|
||||
function getDocDataHash(doc: GeneratedPreviewDocument): string | null {
|
||||
if (!isRecord(doc.meta)) return null;
|
||||
const hash = doc.meta.doc_data_hash;
|
||||
return typeof hash === 'string' && hash.length > 0 ? hash : null;
|
||||
}
|
||||
|
||||
async function generateDocument(registry_id: number, payload: CapitalProgramPrivateData): Promise<GeneratedPreviewDocument> {
|
||||
const { [Mutations.Documents.GenerateDocument.name]: result } = await client.Mutation(
|
||||
Mutations.Documents.GenerateDocument.mutation,
|
||||
{
|
||||
variables: {
|
||||
input: {
|
||||
data: {
|
||||
coopname: system.info?.coopname || '',
|
||||
username: session.username,
|
||||
lang: 'ru',
|
||||
registry_id,
|
||||
doc_data: payload,
|
||||
},
|
||||
options: { skip_save: true },
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
if (!result) {
|
||||
throw new Error('Документ не был сгенерирован');
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
async function generatePreview() {
|
||||
const payload = getPayload();
|
||||
const missing = validatePayload(payload);
|
||||
if (missing.length) {
|
||||
setFieldErrors(missing);
|
||||
FailAlert(`Заполните параметры документов: ${missing.join(', ')}`);
|
||||
return `Заполните все параметры в документах: ${missing.join(', ')}`;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
async function loadPreview(registryId: number, options?: { silent?: boolean }) {
|
||||
if (!options?.silent) {
|
||||
loadingRegistryId.value = registryId;
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await documentApi.generateDocument({
|
||||
coopname: systemStore.info?.coopname ?? '',
|
||||
username: sessionStore.username ?? '',
|
||||
registry_id: registryId,
|
||||
doc_data: getPreviewPayload(),
|
||||
});
|
||||
|
||||
previews[registryId] = {
|
||||
html: result.html,
|
||||
docDataHash: getDocDataHashFromResult(result),
|
||||
};
|
||||
persistDraftToStorage();
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
if (!options?.silent) {
|
||||
$q.notify({
|
||||
type: 'negative',
|
||||
message: error instanceof Error ? error.message : 'Не удалось сформировать предпросмотр',
|
||||
});
|
||||
}
|
||||
throw error;
|
||||
} finally {
|
||||
if (!options?.silent) {
|
||||
loadingRegistryId.value = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function saveParams() {
|
||||
const validationError = validatePayload();
|
||||
if (validationError) {
|
||||
$q.notify({ type: 'warning', message: validationError });
|
||||
return;
|
||||
}
|
||||
|
||||
clearFieldErrors();
|
||||
saving.value = true;
|
||||
|
||||
try {
|
||||
isGenerating.value = true;
|
||||
const docs = await Promise.all([
|
||||
generateDocument(994, payload),
|
||||
generateDocument(998, payload),
|
||||
]);
|
||||
|
||||
const docDataHash = docs
|
||||
.map(getDocDataHash)
|
||||
.find((hash: unknown): hash is string => typeof hash === 'string' && hash.length > 0);
|
||||
|
||||
if (!docDataHash) {
|
||||
throw new Error('Генератор не вернул doc_data_hash');
|
||||
for (const section of DOCUMENT_SECTIONS) {
|
||||
await loadPreview(section.registryId, { silent: true });
|
||||
}
|
||||
|
||||
previewDocuments.value = docs.map((doc, index) => ({
|
||||
registry_id: index === 0 ? 994 : 998,
|
||||
title: doc.full_title,
|
||||
html: doc.html,
|
||||
hash: doc.hash,
|
||||
}));
|
||||
activePreviewTab.value = '994';
|
||||
previewOpen.value = true;
|
||||
emit('update:config', {
|
||||
...props.config,
|
||||
capital_program_doc_data_hash: docDataHash,
|
||||
const hash = previews[994]?.docDataHash ?? previews[998]?.docDataHash;
|
||||
if (!hash) {
|
||||
throw new Error('Не удалось получить хеш параметров документов');
|
||||
}
|
||||
|
||||
await api.saveProgramDocDataHash({ doc_data_hash: hash });
|
||||
|
||||
savedHash.value = hash;
|
||||
emit('saved', hash);
|
||||
|
||||
$q.notify({
|
||||
type: 'positive',
|
||||
message: 'Параметры документов ЦПП сохранены',
|
||||
});
|
||||
SuccessAlert('Параметры документов сохранены в PrivateData');
|
||||
} catch (error) {
|
||||
FailAlert(error);
|
||||
$q.notify({
|
||||
type: 'negative',
|
||||
message: error instanceof Error ? error.message : 'Не удалось сохранить параметры',
|
||||
});
|
||||
} finally {
|
||||
isGenerating.value = false;
|
||||
saving.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
watch(activeTab, (registryId) => {
|
||||
persistDraftToStorage();
|
||||
if (!previews[registryId]) {
|
||||
void loadPreview(registryId);
|
||||
}
|
||||
});
|
||||
|
||||
watch(
|
||||
form,
|
||||
() => {
|
||||
persistDraftToStorage();
|
||||
},
|
||||
{ deep: true },
|
||||
);
|
||||
|
||||
watch(savedHash, () => {
|
||||
persistDraftToStorage();
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
restoreDraftFromStorage();
|
||||
|
||||
if (!previews[activeTab.value]) {
|
||||
void loadPreview(activeTab.value);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.capital-doc-params__intro {
|
||||
margin: 0 0 var(--p-4);
|
||||
color: var(--p-ink-secondary);
|
||||
color: var(--p-ink-muted);
|
||||
}
|
||||
|
||||
.capital-doc-params__section {
|
||||
.capital-doc-params__tabs {
|
||||
margin-bottom: var(--p-2);
|
||||
}
|
||||
|
||||
.capital-doc-params__panels {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.capital-doc-params__toolbar {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--p-3);
|
||||
margin-top: var(--p-2);
|
||||
align-items: center;
|
||||
gap: var(--p-2);
|
||||
}
|
||||
|
||||
.capital-doc-params__footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--p-4);
|
||||
margin-top: var(--p-6);
|
||||
}
|
||||
|
||||
.capital-doc-params__hint {
|
||||
margin: 0;
|
||||
color: var(--p-ink-secondary);
|
||||
}
|
||||
|
||||
.capital-doc-params__tabs {
|
||||
color: var(--p-primary);
|
||||
}
|
||||
|
||||
.doc-preview :deep([data-doc-param]) {
|
||||
background: var(--p-warn-soft);
|
||||
border-radius: var(--p-r-sm);
|
||||
padding: 0 2px;
|
||||
justify-content: flex-end;
|
||||
margin-top: var(--p-4);
|
||||
padding-top: var(--p-4);
|
||||
border-top: 1px solid var(--p-line);
|
||||
}
|
||||
</style>
|
||||
|
||||
+79
@@ -0,0 +1,79 @@
|
||||
<template lang="pug">
|
||||
.capital-doc-step
|
||||
.capital-doc-step__preview-shell
|
||||
.capital-doc-step__toolbar
|
||||
BaseButton(
|
||||
variant='ghost'
|
||||
size='sm'
|
||||
:disable='loading'
|
||||
@click='emit("refresh")'
|
||||
)
|
||||
q-icon(name='refresh' size='18px')
|
||||
span.q-ml-xs {{ previewHtml ? 'Обновить предпросмотр' : 'Сформировать предпросмотр' }}
|
||||
|
||||
CapitalProgramInlineDocumentPreview(
|
||||
v-if='previewHtml && !loading'
|
||||
v-model='form'
|
||||
:html='previewHtml'
|
||||
:field-labels='FIELD_LABELS'
|
||||
)
|
||||
|
||||
.capital-doc-step__loading(v-else-if='loading')
|
||||
q-spinner(size='36px' color='primary')
|
||||
p.t-body-sm.capital-doc-step__loading-text Формируем предпросмотр документа…
|
||||
|
||||
EmptyState(
|
||||
v-else
|
||||
title='Предпросмотр ещё не сформирован'
|
||||
body='Нажмите «Сформировать предпросмотр», чтобы увидеть документ с редактируемыми полями.'
|
||||
)
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { BaseButton, EmptyState } from 'src/shared/ui/base';
|
||||
import CapitalProgramInlineDocumentPreview from './CapitalProgramInlineDocumentPreview.vue';
|
||||
import { FIELD_LABELS, type EditableFieldKey } from '../model/capitalProgramDocFields';
|
||||
|
||||
defineProps<{
|
||||
previewHtml: string;
|
||||
loading: boolean;
|
||||
}>();
|
||||
|
||||
const form = defineModel<Record<EditableFieldKey, string>>({ required: true });
|
||||
|
||||
const emit = defineEmits<{
|
||||
refresh: [];
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.capital-doc-step__preview-shell {
|
||||
border: 1px solid var(--p-line);
|
||||
border-radius: var(--p-r-md);
|
||||
background: var(--p-surface);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.capital-doc-step__toolbar {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
padding: var(--p-2) var(--p-3);
|
||||
border-bottom: 1px solid var(--p-line);
|
||||
background: var(--p-surface-2);
|
||||
}
|
||||
|
||||
.capital-doc-step__loading {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: var(--p-3);
|
||||
min-height: 240px;
|
||||
padding: var(--p-6);
|
||||
}
|
||||
|
||||
.capital-doc-step__loading-text {
|
||||
margin: 0;
|
||||
color: var(--p-ink-2);
|
||||
}
|
||||
</style>
|
||||
+202
@@ -0,0 +1,202 @@
|
||||
<template lang="pug">
|
||||
.capital-inline-doc.statement(ref='containerRef')
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import DOMPurify from 'dompurify';
|
||||
import { nextTick, onBeforeUnmount, ref, watch } from 'vue';
|
||||
|
||||
import type { EditableFieldKey } from '../model/capitalProgramDocFields';
|
||||
|
||||
const props = defineProps<{
|
||||
html: string;
|
||||
fieldLabels: Partial<Record<EditableFieldKey, string>>;
|
||||
}>();
|
||||
|
||||
const form = defineModel<Record<EditableFieldKey, string>>({ required: true });
|
||||
|
||||
const containerRef = ref<HTMLElement | null>(null);
|
||||
const listeners = new WeakMap<HTMLTextAreaElement, () => void>();
|
||||
|
||||
type FocusSnapshot = {
|
||||
key: EditableFieldKey;
|
||||
selectionStart: number;
|
||||
selectionEnd: number;
|
||||
};
|
||||
|
||||
let focusSnapshot: FocusSnapshot | null = null;
|
||||
|
||||
function isFieldEmpty(key: EditableFieldKey): boolean {
|
||||
return !String(form.value[key] ?? '').trim();
|
||||
}
|
||||
|
||||
function applyFieldStyle(textarea: HTMLTextAreaElement, key: EditableFieldKey) {
|
||||
textarea.classList.toggle('capital-doc-param-inline--empty', isFieldEmpty(key));
|
||||
}
|
||||
|
||||
function captureFocus() {
|
||||
const active = document.activeElement;
|
||||
if (!(active instanceof HTMLTextAreaElement) || !active.classList.contains('capital-doc-param-inline')) {
|
||||
focusSnapshot = null;
|
||||
return;
|
||||
}
|
||||
|
||||
const key = active.getAttribute('data-field') as EditableFieldKey | null;
|
||||
if (!key) {
|
||||
focusSnapshot = null;
|
||||
return;
|
||||
}
|
||||
|
||||
focusSnapshot = {
|
||||
key,
|
||||
selectionStart: active.selectionStart,
|
||||
selectionEnd: active.selectionEnd,
|
||||
};
|
||||
}
|
||||
|
||||
function restoreFocus(root: HTMLElement) {
|
||||
if (!focusSnapshot) return;
|
||||
|
||||
const textarea = root.querySelector(`textarea[data-field="${focusSnapshot.key}"]`) as HTMLTextAreaElement | null;
|
||||
if (!textarea) return;
|
||||
|
||||
textarea.focus();
|
||||
textarea.setSelectionRange(focusSnapshot.selectionStart, focusSnapshot.selectionEnd);
|
||||
}
|
||||
|
||||
function cleanupEditors(root: HTMLElement) {
|
||||
root.querySelectorAll('textarea.capital-doc-param-inline').forEach((node) => {
|
||||
const textarea = node as HTMLTextAreaElement;
|
||||
const handler = listeners.get(textarea);
|
||||
if (handler) {
|
||||
textarea.removeEventListener('input', handler);
|
||||
listeners.delete(textarea);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function mountInlineEditors(root: HTMLElement) {
|
||||
root.querySelectorAll('[data-doc-param]').forEach((node) => {
|
||||
const key = node.getAttribute('data-doc-param') as EditableFieldKey | null;
|
||||
if (!key) return;
|
||||
|
||||
const textarea = document.createElement('textarea');
|
||||
textarea.className = 'capital-doc-param-inline';
|
||||
textarea.setAttribute('data-field', key);
|
||||
textarea.rows =
|
||||
key.includes('description') ||
|
||||
key.includes('expansion') ||
|
||||
key.includes('development') ||
|
||||
key.includes('goal') ||
|
||||
key.includes('purpose')
|
||||
? 3
|
||||
: 2;
|
||||
textarea.value = form.value[key] ?? '';
|
||||
textarea.placeholder = props.fieldLabels[key] ?? key;
|
||||
applyFieldStyle(textarea, key);
|
||||
|
||||
const onInput = () => {
|
||||
form.value[key] = textarea.value;
|
||||
applyFieldStyle(textarea, key);
|
||||
};
|
||||
textarea.addEventListener('input', onInput);
|
||||
listeners.set(textarea, onInput);
|
||||
|
||||
node.replaceWith(textarea);
|
||||
});
|
||||
}
|
||||
|
||||
function syncInlineEditors(root: HTMLElement) {
|
||||
root.querySelectorAll('textarea.capital-doc-param-inline').forEach((node) => {
|
||||
const textarea = node as HTMLTextAreaElement;
|
||||
const key = textarea.getAttribute('data-field') as EditableFieldKey | null;
|
||||
if (!key) return;
|
||||
|
||||
if (textarea !== document.activeElement && textarea.value !== (form.value[key] ?? '')) {
|
||||
textarea.value = form.value[key] ?? '';
|
||||
}
|
||||
|
||||
applyFieldStyle(textarea, key);
|
||||
});
|
||||
}
|
||||
|
||||
async function renderDocument(html: string) {
|
||||
await nextTick();
|
||||
const root = containerRef.value;
|
||||
if (!root) return;
|
||||
|
||||
captureFocus();
|
||||
cleanupEditors(root);
|
||||
root.innerHTML = DOMPurify.sanitize(html);
|
||||
mountInlineEditors(root);
|
||||
restoreFocus(root);
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.html,
|
||||
(html) => {
|
||||
if (html) void renderDocument(html);
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
watch(
|
||||
form,
|
||||
() => {
|
||||
if (containerRef.value) syncInlineEditors(containerRef.value);
|
||||
},
|
||||
{ deep: true },
|
||||
);
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (containerRef.value) cleanupEditors(containerRef.value);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.capital-inline-doc {
|
||||
max-height: 60vh;
|
||||
overflow: auto;
|
||||
padding: var(--p-4);
|
||||
border: 1px solid var(--p-line);
|
||||
border-radius: var(--p-r-md);
|
||||
background: var(--p-surface);
|
||||
}
|
||||
|
||||
.capital-inline-doc :deep(.capital-doc-param-inline) {
|
||||
display: block;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
margin: var(--p-1) 0;
|
||||
padding: var(--p-2) var(--p-3);
|
||||
font: inherit;
|
||||
line-height: inherit;
|
||||
color: var(--p-ink);
|
||||
background: color-mix(in srgb, var(--p-primary) 8%, var(--p-surface));
|
||||
border: 1px dashed var(--p-primary);
|
||||
border-radius: var(--p-r-sm);
|
||||
resize: vertical;
|
||||
box-sizing: border-box;
|
||||
white-space: pre-wrap;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
border-style: solid;
|
||||
background: color-mix(in srgb, var(--p-primary) 12%, var(--p-surface));
|
||||
}
|
||||
|
||||
&::placeholder {
|
||||
color: var(--p-ink-muted);
|
||||
font-size: 0.85em;
|
||||
}
|
||||
}
|
||||
|
||||
.capital-inline-doc :deep(.capital-doc-param-inline--empty) {
|
||||
border-color: var(--p-warn);
|
||||
background: color-mix(in srgb, var(--p-warn) 12%, var(--p-surface));
|
||||
|
||||
&:focus {
|
||||
background: color-mix(in srgb, var(--p-warn) 16%, var(--p-surface));
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,2 +1,4 @@
|
||||
export { default as CapitalCouncilStepGroup } from './CapitalCouncilStepGroup.vue';
|
||||
export { default as CapitalOnboardingCard } from './CapitalOnboardingCard.vue';
|
||||
export { default as CapitalProgramDocumentParametersWidget } from './CapitalProgramDocumentParametersWidget.vue';
|
||||
export { default as CapitalProgramDocumentStepPanel } from './CapitalProgramDocumentStepPanel.vue';
|
||||
export { default as CapitalProgramInlineDocumentPreview } from './CapitalProgramInlineDocumentPreview.vue';
|
||||
|
||||
@@ -36,9 +36,10 @@ const props = defineProps<{
|
||||
mask?: string
|
||||
/** Подставлять плейсхолдеры маски при вводе */
|
||||
fillMask?: boolean
|
||||
/** Разрешить только цифры и точку (ОКВЭД: 94.99, 46.73.7). Блокирует
|
||||
* keypress/paste — буквы вообще не появляются в поле. */
|
||||
digitsDotsOnly?: boolean
|
||||
/** Разрешить только цифры + перечисленные доп. символы (напр. '.' для
|
||||
* ОКВЭД: 94.99, 46.73.7; '-' для Рег. номера СФР). Блокирует
|
||||
* keypress/paste — посторонние символы вообще не появляются в поле. */
|
||||
digitsExtraChars?: string
|
||||
/** HTML maxlength. */
|
||||
maxLength?: number
|
||||
/** Допустимые точные длины (ИЛИ). Напр. `[8, 11]` для ОКТМО. */
|
||||
@@ -60,12 +61,20 @@ const labelText = computed(() =>
|
||||
)
|
||||
|
||||
const inputModeComputed = computed<'numeric' | 'decimal' | 'text'>(() => {
|
||||
if (props.digitsDotsOnly) return 'decimal'
|
||||
// '.' — десятичная клавиатура мобильных устройств её показывает; для
|
||||
// прочих доп.символов (напр. '-') нужна полная клавиатура — 'text'.
|
||||
if (props.digitsExtraChars === '.') return 'decimal'
|
||||
if (props.digitsExtraChars) return 'text'
|
||||
// mask из одних `#` = чистая цифровая клавиатура на мобиле.
|
||||
if (props.mask && /^#+$/.test(props.mask)) return 'numeric'
|
||||
return 'text'
|
||||
})
|
||||
|
||||
/** Экранирует спецсимволы regex, чтобы вставить строку в символьный класс `[...]`. */
|
||||
function escapeForCharClass(chars: string): string {
|
||||
return chars.replace(/[\]\\^-]/g, '\\$&')
|
||||
}
|
||||
|
||||
const NAV_KEYS = new Set([
|
||||
'Backspace',
|
||||
'Delete',
|
||||
@@ -81,19 +90,21 @@ const NAV_KEYS = new Set([
|
||||
])
|
||||
|
||||
// Блокируем keypress не-допустимых символов — буквы не попадают в поле даже
|
||||
// на миллисекунды. Работает только для digitsDotsOnly (для чисто цифровых
|
||||
// на миллисекунды. Работает только для digitsExtraChars (для чисто цифровых
|
||||
// полей используем Quasar-mask — он нативно блокирует не-# символы).
|
||||
function onKeydown(e: KeyboardEvent): void {
|
||||
if (!props.digitsDotsOnly) return
|
||||
if (!props.digitsExtraChars) return
|
||||
if (e.ctrlKey || e.metaKey || e.altKey) return
|
||||
if (NAV_KEYS.has(e.key)) return
|
||||
if (!/^[\d.]$/.test(e.key)) e.preventDefault()
|
||||
const re = new RegExp(`^[\\d${escapeForCharClass(props.digitsExtraChars)}]$`)
|
||||
if (!re.test(e.key)) e.preventDefault()
|
||||
}
|
||||
|
||||
function onPaste(e: ClipboardEvent): void {
|
||||
if (!props.digitsDotsOnly) return
|
||||
if (!props.digitsExtraChars) return
|
||||
const text = e.clipboardData?.getData('text') ?? ''
|
||||
if (!/^[\d.]*$/.test(text)) e.preventDefault()
|
||||
const re = new RegExp(`^[\\d${escapeForCharClass(props.digitsExtraChars)}]*$`)
|
||||
if (!re.test(text)) e.preventDefault()
|
||||
}
|
||||
|
||||
const rules = computed(() => {
|
||||
@@ -128,7 +139,10 @@ function onInput(raw: string | number | null): void {
|
||||
// Фоллбек-фильтр на случай programmatic ввода (drop/autofill). Для ручного
|
||||
// набора актуальна либо mask (q-input сам режет), либо keydown-блокер.
|
||||
let s = String(raw ?? '')
|
||||
if (props.digitsDotsOnly) s = s.replace(/[^\d.]+/g, '')
|
||||
if (props.digitsExtraChars) {
|
||||
const re = new RegExp(`[^\\d${escapeForCharClass(props.digitsExtraChars)}]+`, 'g')
|
||||
s = s.replace(re, '')
|
||||
}
|
||||
if (props.maxLength && s.length > props.maxLength) s = s.slice(0, props.maxLength)
|
||||
emit('update:value', s)
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@ q-form.settings-page(@submit.prevent='save' @validation-error='onValidationError
|
||||
:value='manualInput[f.key]'
|
||||
:placeholder='f.placeholder'
|
||||
:mask='f.mask'
|
||||
:digits-dots-only='f.digitsDotsOnly'
|
||||
:digits-extra-chars='f.digitsExtraChars'
|
||||
:max-length='f.maxLength'
|
||||
:exact-lengths='f.exactLengths'
|
||||
:pattern='f.pattern'
|
||||
@@ -60,10 +60,11 @@ q-form.settings-page(@submit.prevent='save' @validation-error='onValidationError
|
||||
id='field-sfrRegNumber'
|
||||
label='Рег. номер СФР'
|
||||
:value='manualInput.sfrRegNumber'
|
||||
placeholder='XXX-XXX-XXXXXX'
|
||||
mask='###-###-######'
|
||||
placeholder='XXX-XXX-XXXXXX или 10 цифр'
|
||||
digits-extra-chars='-'
|
||||
:max-length='14'
|
||||
:pattern='SFR_REG_PATTERN'
|
||||
pattern-message='Формат: XXX-XXX-XXXXXX (12 цифр)'
|
||||
pattern-message='Формат: XXX-XXX-XXXXXX (12 цифр) или 10 цифр без разделителей'
|
||||
required
|
||||
@update:value='v => (manualInput.sfrRegNumber = v)'
|
||||
)
|
||||
@@ -202,7 +203,7 @@ interface ClassifierField {
|
||||
label: string
|
||||
placeholder?: string
|
||||
mask?: string
|
||||
digitsDotsOnly?: boolean
|
||||
digitsExtraChars?: string
|
||||
maxLength?: number
|
||||
exactLengths?: number[]
|
||||
pattern?: RegExp
|
||||
@@ -212,13 +213,13 @@ interface ClassifierField {
|
||||
// Placeholder'ы — правило ввода (кол-во цифр), а не конкретные примеры.
|
||||
// Для цифровых полей фиксированной/короткой длины — используем Quasar-mask
|
||||
// (`#` = только цифра, буквы блокируются на keypress нативно).
|
||||
// Для ОКВЭД (цифры + точка переменной длины) — digitsDotsOnly с @keydown-блокером.
|
||||
// Для ОКВЭД (цифры + точка переменной длины) — digitsExtraChars='.' с @keydown-блокером.
|
||||
const classifierFields: ClassifierField[] = [
|
||||
{
|
||||
key: 'okved',
|
||||
label: 'ОКВЭД',
|
||||
placeholder: 'XX.XX или XX.XX.XX',
|
||||
digitsDotsOnly: true,
|
||||
digitsExtraChars: '.',
|
||||
maxLength: 8,
|
||||
pattern: /^\d{2}(\.\d{1,2}){0,2}$/,
|
||||
patternMessage: 'Формат: XX.XX или XX.XX.XX',
|
||||
@@ -265,7 +266,7 @@ const signerTypeOptions = [
|
||||
// пользователь прожал пару цифр и нажал «Сохранить»: mask не мешает
|
||||
// сохранить частичный ввод, паттерн-правило блокирует.
|
||||
const SNILS_PATTERN = /^\d{3}-\d{3}-\d{3} \d{2}$/
|
||||
const SFR_REG_PATTERN = /^\d{3}-\d{3}-\d{6}$/
|
||||
const SFR_REG_PATTERN = /^(\d{3}-\d{3}-\d{6}|\d{10})$/
|
||||
|
||||
function getValue(key: keyof IReportRequisitesView): string {
|
||||
const v = requisites.value?.[key] as any
|
||||
|
||||
@@ -162,8 +162,8 @@
|
||||
:rules='[reportRules.sfrRegNumber()]'
|
||||
:error='errFor("signer.sfrRegNumber")'
|
||||
:error-message='msgFor("signer.sfrRegNumber")'
|
||||
mask='###-###-######'
|
||||
hint='XXX-XXX-XXXXXX — обязателен для ЕФС-1'
|
||||
maxlength='14'
|
||||
hint='XXX-XXX-XXXXXX или 10 цифр — обязателен для ЕФС-1'
|
||||
dense filled
|
||||
)
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@coopenomics/desktop",
|
||||
"version": "2026.6.24",
|
||||
"version": "2026.7.21",
|
||||
"description": "Рабочий стол кооператива — Vue 3 + Quasar Framework",
|
||||
"productName": "Desktop App",
|
||||
"author": "Alex Ant <dacom.dark.sun@gmail.com>",
|
||||
@@ -95,7 +95,6 @@
|
||||
"@types/serviceworker": "^0.0.141",
|
||||
"@typescript-eslint/eslint-plugin": "^8.57.0",
|
||||
"@typescript-eslint/parser": "^8.57.0",
|
||||
"@vue/devtools": "^8.1.1",
|
||||
"@vue/language-plugin-pug": "~3.2.6",
|
||||
"autoprefixer": "^10.4.2",
|
||||
"concurrently": "^8.2.2",
|
||||
|
||||
@@ -7,18 +7,19 @@ BaseDialog(
|
||||
:close-on-backdrop='false',
|
||||
:close-on-escape='false'
|
||||
)
|
||||
div.row.justify-center
|
||||
div(style="padding-bottom: 100px;").col-md-8.col-col-xs-12
|
||||
div.row.justify-center.select-branch-overlay__content
|
||||
div.col-md-8.col-xs-12
|
||||
|
||||
div(v-if="step === 1")
|
||||
p.q-mt-lg
|
||||
| Кооператив перешёл на двухэтапную систему управления на основании общего собрания уполномоченных председателей кооперативных участков...
|
||||
Form(
|
||||
Loader(v-if="branchesLoading" text="Загружаем список кооперативных участков...")
|
||||
Form#select-branch-form(
|
||||
v-else
|
||||
:handler-submit="next"
|
||||
:is-submitting="isSubmitting"
|
||||
:showSubmit="!isLoading"
|
||||
:showSubmit="false"
|
||||
:showCancel="false"
|
||||
:button-submit-txt="'Продолжить'"
|
||||
)
|
||||
BranchSelector(
|
||||
v-model:selectedBranch="selectedBranch"
|
||||
@@ -27,15 +28,29 @@ BaseDialog(
|
||||
|
||||
div(v-else-if="step === 2")
|
||||
Loader(v-if="isLoading" :text="`Формируем документ...`")
|
||||
DocumentHtmlReader(v-else :html="document.html")
|
||||
|
||||
div(v-else)
|
||||
DocumentHtmlReader(:html="document.html")
|
||||
q-btn(@click="back" flat) назад
|
||||
q-btn(@click="sign" color="primary") подписать
|
||||
template(#footer)
|
||||
.select-branch-overlay__actions(v-if="step === 1 && !branchesLoading")
|
||||
BaseButton(
|
||||
variant='primary',
|
||||
type='submit',
|
||||
form='select-branch-form',
|
||||
:loading='isLoading',
|
||||
:disabled='!selectedBranch || isLoading',
|
||||
) Продолжить
|
||||
.select-branch-overlay__actions(v-else-if="step === 2 && !isLoading")
|
||||
BaseButton(variant='ghost', @click='back') назад
|
||||
BaseButton(
|
||||
variant='primary',
|
||||
:loading='isSubmitting',
|
||||
@click='sign'
|
||||
) подписать
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { BaseDialog } from 'src/shared/ui/base/BaseDialog'
|
||||
import { BaseButton } from 'src/shared/ui/base/BaseButton'
|
||||
import { Form } from 'src/shared/ui/Form'
|
||||
import { Loader } from 'src/shared/ui/Loader'
|
||||
import { BranchSelector } from 'src/shared/ui/BranchSelector'
|
||||
@@ -53,12 +68,30 @@ BaseDialog(
|
||||
document,
|
||||
isSubmitting,
|
||||
isLoading,
|
||||
branchesLoading,
|
||||
next,
|
||||
back,
|
||||
sign
|
||||
} = useSelectBranchProcess()
|
||||
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
/* Кнопки в слоте footer BaseDialog — вне скроллящегося body,
|
||||
поэтому «Продолжить» / «подписать» всегда видны на мобильных. */
|
||||
.select-branch-overlay__actions {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
gap: var(--p-2, 8px);
|
||||
justify-content: flex-end;
|
||||
flex-wrap: wrap;
|
||||
padding-bottom: env(safe-area-inset-bottom, 0px);
|
||||
}
|
||||
|
||||
.select-branch-overlay__content {
|
||||
padding-bottom: var(--p-2, 8px);
|
||||
}
|
||||
</style>
|
||||
|
||||
<style>
|
||||
.digital-document .header{
|
||||
text-align: center;
|
||||
|
||||
@@ -236,9 +236,7 @@ const goToCooperativeSite = () => {
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
padding: 1rem;
|
||||
background: rgba(255, 255, 255, 0.5);
|
||||
border-radius: 8px;
|
||||
border: 1px solid rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
/* Адаптивность */
|
||||
|
||||
@@ -20,31 +20,62 @@ export function useSelectBranchProcess() {
|
||||
const system = useSystemStore()
|
||||
const session = useSessionStore()
|
||||
const branchStore = useBranchStore()
|
||||
const { selectBranch } = useSelectBranch()
|
||||
const { isVisible, selectBranch } = useSelectBranch()
|
||||
|
||||
const branches = computed(() => branchStore.publicBranches)
|
||||
const branchesLoading = ref(false)
|
||||
|
||||
// Вотчер на авторизацию
|
||||
const loadBranches = async () => {
|
||||
if (!session.isAuth || !system.info.coopname) return
|
||||
|
||||
try {
|
||||
branchesLoading.value = true
|
||||
await branchStore.loadPublicBranches({ coopname: system.info.coopname })
|
||||
} catch (e: unknown) {
|
||||
FailAlert(e)
|
||||
} finally {
|
||||
branchesLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// Загрузка при входе
|
||||
watch(
|
||||
() => session.isAuth,
|
||||
(isAuth, wasAuth) => {
|
||||
if (isAuth && !wasAuth) {
|
||||
branchStore.loadPublicBranches({ coopname: system.info.coopname })
|
||||
void loadBranches()
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
// Перезагрузка при переходе в мажоритарный режим или показе оверлея
|
||||
watch(
|
||||
[() => isVisible.value, () => system.info?.cooperator_account?.is_branched],
|
||||
([visible, branched]) => {
|
||||
if (visible && branched && session.isAuth) {
|
||||
void loadBranches()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
const next = async () => {
|
||||
isLoading.value = true
|
||||
document.value = await digitalDocument.generate<Cooperative.Registry.SelectBranchStatement.Action>({
|
||||
registry_id: Cooperative.Registry.SelectBranchStatement.registry_id,
|
||||
coopname: system.info.coopname,
|
||||
username: session.username,
|
||||
braname: selectedBranch.value,
|
||||
})
|
||||
isLoading.value = false
|
||||
step.value++
|
||||
if (!selectedBranch.value || isLoading.value) return
|
||||
|
||||
try {
|
||||
isLoading.value = true
|
||||
document.value = await digitalDocument.generate<Cooperative.Registry.SelectBranchStatement.Action>({
|
||||
registry_id: Cooperative.Registry.SelectBranchStatement.registry_id,
|
||||
coopname: system.info.coopname,
|
||||
username: session.username,
|
||||
braname: selectedBranch.value,
|
||||
})
|
||||
step.value++
|
||||
} catch (e: unknown) {
|
||||
FailAlert(e)
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const back = () => {
|
||||
@@ -80,6 +111,7 @@ export function useSelectBranchProcess() {
|
||||
document,
|
||||
isSubmitting,
|
||||
isLoading,
|
||||
branchesLoading,
|
||||
next,
|
||||
back,
|
||||
sign,
|
||||
|
||||
@@ -9,7 +9,6 @@ div
|
||||
emit-value
|
||||
standout="bg-teal text-white"
|
||||
map-options
|
||||
@update:model-value="$emit('update:selectedBranch', $event)"
|
||||
)
|
||||
|
||||
template(v-slot:option="scope")
|
||||
@@ -27,22 +26,13 @@ div
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { type IPublicBranch } from 'src/entities/Branch/model';
|
||||
import { ref, watch } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
defineProps({
|
||||
branches: {
|
||||
type: Object as () => IPublicBranch[],
|
||||
required: true,
|
||||
},
|
||||
modelValue: {
|
||||
type: String,
|
||||
required: false,
|
||||
},
|
||||
})
|
||||
|
||||
const selectedBranch = ref(props.modelValue)
|
||||
|
||||
watch(() => props.modelValue, (newVal) => {
|
||||
selectedBranch.value = newVal
|
||||
})
|
||||
const selectedBranch = defineModel<string>('selectedBranch', { default: '' })
|
||||
</script>
|
||||
|
||||
@@ -66,21 +66,23 @@ const maxWidthBySize: Record<BaseDialogSize, string> = {
|
||||
<style scoped>
|
||||
.base-dialog {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-height: calc(100vh - var(--p-6, 48px));
|
||||
}
|
||||
/* Maximized: контейнер растягивается на весь экран, контент скроллится. */
|
||||
.base-dialog--maximized {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
max-width: none;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-height: none;
|
||||
}
|
||||
.base-dialog--maximized .base-dialog__body {
|
||||
flex: 1 1 auto;
|
||||
overflow: auto;
|
||||
.base-dialog--maximized .base-dialog__foot {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.base-dialog__head {
|
||||
display: flex;
|
||||
flex: 0 0 auto;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--p-3, 12px);
|
||||
@@ -94,13 +96,19 @@ const maxWidthBySize: Record<BaseDialogSize, string> = {
|
||||
}
|
||||
.base-dialog__body {
|
||||
display: flex;
|
||||
flex: 1 1 auto;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
padding-bottom: var(--p-2, 8px);
|
||||
}
|
||||
.base-dialog__body > :first-child + * {
|
||||
margin-top: var(--p-4, 16px);
|
||||
}
|
||||
.base-dialog__foot {
|
||||
padding-top: var(--p-2, 8px);
|
||||
flex: 0 0 auto;
|
||||
padding: var(--p-3, 12px) var(--p-4, 16px);
|
||||
border-top: 1px solid var(--p-line);
|
||||
background: var(--p-surface);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,11 +1,6 @@
|
||||
<template lang="pug">
|
||||
.extension-install
|
||||
q-form(:ref='setFormRef')
|
||||
CapitalProgramDocumentParametersWidget(
|
||||
v-if='extensionName === "capital"'
|
||||
:config='config'
|
||||
@update:config='$emit("update:config", $event)'
|
||||
)
|
||||
q-form(ref='formRef')
|
||||
BaseCard(v-if='schema && !isEmpty', title='Настройки')
|
||||
ZodForm(
|
||||
:schema='schema',
|
||||
@@ -23,7 +18,7 @@ import type { ComponentPublicInstance } from 'vue';
|
||||
import { ZodForm } from 'src/shared/ui/ZodForm';
|
||||
import { BaseCard } from 'src/shared/ui/base/BaseCard';
|
||||
import { isExtensionSchemaEmpty } from 'src/shared/lib/utils';
|
||||
import { CapitalProgramDocumentParametersWidget } from 'app/extensions/capital/features/Onboarding';
|
||||
import type { IExtensionConfigSchema } from 'src/entities/Extension/model';
|
||||
|
||||
type ExtensionInstallConfig = Record<string, unknown>;
|
||||
type ExtensionInstallFormRef = {
|
||||
@@ -32,7 +27,7 @@ type ExtensionInstallFormRef = {
|
||||
|
||||
interface Props {
|
||||
extensionName?: string;
|
||||
schema?: unknown;
|
||||
schema?: IExtensionConfigSchema;
|
||||
config: ExtensionInstallConfig;
|
||||
formRef?: ExtensionInstallFormRef;
|
||||
}
|
||||
@@ -40,12 +35,6 @@ interface Props {
|
||||
const props = defineProps<Props>();
|
||||
|
||||
const isEmpty = computed(() => isExtensionSchemaEmpty(props.schema));
|
||||
|
||||
const setFormRef = (element: Element | ComponentPublicInstance | null) => {
|
||||
if (props.formRef && 'value' in props.formRef) {
|
||||
props.formRef.value = element;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@coopenomics/docs-harness",
|
||||
"version": "2026.6.24",
|
||||
"version": "2026.7.21",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@coopenomics/docs-harness",
|
||||
"version": "2026.6.24",
|
||||
"version": "2026.7.21",
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.59.1",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@coopenomics/docs-harness",
|
||||
"version": "2026.6.24",
|
||||
"version": "2026.7.21",
|
||||
"private": true,
|
||||
"description": "Playwright harness для съёмки скриншотов в документацию (см. bin/shoot.mjs)",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@coopenomics/docs",
|
||||
"type": "module",
|
||||
"version": "2026.6.24",
|
||||
"version": "2026.7.21",
|
||||
"private": true,
|
||||
"packageManager": "pnpm@9.9.0",
|
||||
"description": "Документация для монорепозитория",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@coopenomics/factory",
|
||||
"type": "module",
|
||||
"version": "2026.6.24",
|
||||
"version": "2026.7.21",
|
||||
"description": "Фабрика юридических документов кооператива",
|
||||
"author": "Alex Ant <chairman.voskhod@gmail.com>",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -12,18 +12,24 @@ export class Factory extends DocFactory<BlagorostProgramTemplate.Action> {
|
||||
}
|
||||
|
||||
async generateDocument(data: BlagorostProgramTemplate.Action, options?: IGenerationOptions): Promise<IGeneratedDocument> {
|
||||
const { template, vars, docData } = await this.resolveParallel({
|
||||
const { template, vars, coop, docData } = await this.resolveParallel({
|
||||
template: () => process.env.SOURCE === 'local'
|
||||
? Promise.resolve(BlagorostProgramTemplate.Template as ITemplate<BlagorostProgramTemplate.Model>)
|
||||
: this.getTemplate<BlagorostProgramTemplate.Model>(DraftContract.contractName.production, BlagorostProgramTemplate.registry_id, data.block_num),
|
||||
vars: () => this.getVars(data.coopname),
|
||||
coop: () => this.getCooperative(data.coopname),
|
||||
docData: () => this.loadDocData<BlagorostProgramTemplate.Model['doc_data']>(data),
|
||||
})
|
||||
|
||||
const meta: IMetaDocument = await this.getMeta({ title: template.title, ...data })
|
||||
const doc_data = Cooperative.Registry.requireCapitalProgramPrivateData(docData, BlagorostProgramTemplate.registry_id)
|
||||
|
||||
const combinedData: BlagorostProgramTemplate.Model = { meta, vars, doc_data }
|
||||
const combinedData: BlagorostProgramTemplate.Model = {
|
||||
meta,
|
||||
coop,
|
||||
vars,
|
||||
doc_data,
|
||||
}
|
||||
|
||||
await this.validate(combinedData, template.model)
|
||||
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import type { JSONSchemaType } from 'ajv'
|
||||
import { Cooperative } from 'cooptypes'
|
||||
|
||||
export const CapitalApprovalHeaderSchema: JSONSchemaType<Cooperative.Registry.CapitalApprovalHeader> = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
protocol_number: { type: 'string' },
|
||||
protocol_day_month_year: { type: 'string' },
|
||||
},
|
||||
required: ['protocol_number', 'protocol_day_month_year'],
|
||||
additionalProperties: false,
|
||||
}
|
||||
|
||||
export const CapitalProgramPrivateDataSchema: JSONSchemaType<Cooperative.Registry.CapitalProgramPrivateData> = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
generator_program_purpose: { type: 'string' },
|
||||
eoap_definition: { type: 'string' },
|
||||
generator_task_goal: { type: 'string' },
|
||||
idea_unit_cost: { type: 'string' },
|
||||
idea_unit_cost_words: { type: 'string' },
|
||||
blagorost_goal_expansion: { type: 'string' },
|
||||
blagorost_goal_reason: { type: 'string' },
|
||||
blagorost_task_expansion: { type: 'string' },
|
||||
blagorost_task_development: { type: 'string' },
|
||||
return_source_description: { type: 'string' },
|
||||
return_additional_source: { type: 'string' },
|
||||
},
|
||||
required: [...Cooperative.Registry.capitalProgramPrivateDataRequiredFields],
|
||||
additionalProperties: false,
|
||||
}
|
||||
@@ -1,46 +1,16 @@
|
||||
import type { JSONSchemaType } from 'ajv'
|
||||
import { Cooperative } from 'cooptypes'
|
||||
import type { IMetaDocument, ITemplate } from '../Interfaces'
|
||||
import type { ITemplate } from '../Interfaces'
|
||||
import { IMetaJSONSchema } from '../Schema/MetaSchema'
|
||||
import { CapitalProgramPrivateDataSchema } from '../Schema/CapitalProgramPrivateDataSchema'
|
||||
import { CommonUserSchema, CooperativeSchema, VarsSchema } from '../Schema'
|
||||
|
||||
export const registry_id = Cooperative.Registry.BlagorostOffer.registry_id
|
||||
|
||||
// Модель действия для генерации
|
||||
export type Action = Cooperative.Registry.BlagorostOffer.Action
|
||||
|
||||
// Модель данных - используем полную модель с дополнительными полями
|
||||
export type Model = Cooperative.Registry.BlagorostOffer.Model
|
||||
|
||||
const CapitalProgramPrivateDataSchema: JSONSchemaType<Cooperative.Registry.CapitalProgramPrivateData> = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
approval_protocol_number: { type: 'string' },
|
||||
approval_protocol_day: { type: 'string' },
|
||||
approval_protocol_month: { type: 'string' },
|
||||
approval_protocol_year: { type: 'string' },
|
||||
cooperative_name: { type: 'string' },
|
||||
cooperative_short_name: { type: 'string' },
|
||||
cooperative_quoted_name: { type: 'string' },
|
||||
website: { type: 'string' },
|
||||
chairman_full_name: { type: 'string' },
|
||||
generator_program_purpose: { type: 'string' },
|
||||
eoap_definition: { type: 'string' },
|
||||
generator_task_goal: { type: 'string' },
|
||||
idea_unit_cost: { type: 'string' },
|
||||
idea_unit_cost_words: { type: 'string' },
|
||||
blagorost_goal_expansion: { type: 'string' },
|
||||
blagorost_task_expansion: { type: 'string' },
|
||||
blagorost_task_development: { type: 'string' },
|
||||
return_source_description: { type: 'string' },
|
||||
return_additional_source: { type: 'string' },
|
||||
offer_template_number: { type: 'string' },
|
||||
},
|
||||
required: Cooperative.Registry.capitalProgramPrivateDataRequiredFields,
|
||||
additionalProperties: false,
|
||||
}
|
||||
|
||||
// Схема для сверки - используем расширенную схему для дополнительных данных
|
||||
export const Schema: JSONSchemaType<Model> = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { JSONSchemaType } from 'ajv'
|
||||
import { Cooperative } from 'cooptypes'
|
||||
import type { ITemplate } from '../Interfaces'
|
||||
import { IMetaJSONSchema } from '../Schema/MetaSchema'
|
||||
import { CapitalProgramPrivateDataSchema } from '../Schema/CapitalProgramPrivateDataSchema'
|
||||
import { CommonUserSchema, CooperativeSchema, VarsSchema } from '../Schema'
|
||||
|
||||
export const registry_id = Cooperative.Registry.GeneratorProgramTemplate.registry_id
|
||||
@@ -12,34 +13,6 @@ export type Action = Cooperative.Registry.GeneratorProgramTemplate.Action
|
||||
// Модель данных
|
||||
export type Model = Cooperative.Registry.GeneratorProgramTemplate.Model
|
||||
|
||||
const CapitalProgramPrivateDataSchema: JSONSchemaType<Cooperative.Registry.CapitalProgramPrivateData> = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
approval_protocol_number: { type: 'string' },
|
||||
approval_protocol_day: { type: 'string' },
|
||||
approval_protocol_month: { type: 'string' },
|
||||
approval_protocol_year: { type: 'string' },
|
||||
cooperative_name: { type: 'string' },
|
||||
cooperative_short_name: { type: 'string' },
|
||||
cooperative_quoted_name: { type: 'string' },
|
||||
website: { type: 'string' },
|
||||
chairman_full_name: { type: 'string' },
|
||||
generator_program_purpose: { type: 'string' },
|
||||
eoap_definition: { type: 'string' },
|
||||
generator_task_goal: { type: 'string' },
|
||||
idea_unit_cost: { type: 'string' },
|
||||
idea_unit_cost_words: { type: 'string' },
|
||||
blagorost_goal_expansion: { type: 'string' },
|
||||
blagorost_task_expansion: { type: 'string' },
|
||||
blagorost_task_development: { type: 'string' },
|
||||
return_source_description: { type: 'string' },
|
||||
return_additional_source: { type: 'string' },
|
||||
offer_template_number: { type: 'string' },
|
||||
},
|
||||
required: Cooperative.Registry.capitalProgramPrivateDataRequiredFields,
|
||||
additionalProperties: false,
|
||||
}
|
||||
|
||||
// Схема для сверки
|
||||
export const Schema: JSONSchemaType<Model> = {
|
||||
type: 'object',
|
||||
|
||||
@@ -2,45 +2,15 @@ import type { JSONSchemaType } from 'ajv'
|
||||
import { Cooperative } from 'cooptypes'
|
||||
import type { ITemplate } from '../Interfaces'
|
||||
import { IMetaJSONSchema } from '../Schema/MetaSchema'
|
||||
import { CapitalProgramPrivateDataSchema } from '../Schema/CapitalProgramPrivateDataSchema'
|
||||
import { CommonUserSchema, CooperativeSchema, VarsSchema } from '../Schema'
|
||||
|
||||
export const registry_id = Cooperative.Registry.GeneratorOfferTemplate.registry_id
|
||||
|
||||
// Модель действия для генерации
|
||||
export type Action = Cooperative.Registry.GeneratorOfferTemplate.Action
|
||||
|
||||
// Модель данных
|
||||
export type Model = Cooperative.Registry.GeneratorOfferTemplate.Model
|
||||
|
||||
const CapitalProgramPrivateDataSchema: JSONSchemaType<Cooperative.Registry.CapitalProgramPrivateData> = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
approval_protocol_number: { type: 'string' },
|
||||
approval_protocol_day: { type: 'string' },
|
||||
approval_protocol_month: { type: 'string' },
|
||||
approval_protocol_year: { type: 'string' },
|
||||
cooperative_name: { type: 'string' },
|
||||
cooperative_short_name: { type: 'string' },
|
||||
cooperative_quoted_name: { type: 'string' },
|
||||
website: { type: 'string' },
|
||||
chairman_full_name: { type: 'string' },
|
||||
generator_program_purpose: { type: 'string' },
|
||||
eoap_definition: { type: 'string' },
|
||||
generator_task_goal: { type: 'string' },
|
||||
idea_unit_cost: { type: 'string' },
|
||||
idea_unit_cost_words: { type: 'string' },
|
||||
blagorost_goal_expansion: { type: 'string' },
|
||||
blagorost_task_expansion: { type: 'string' },
|
||||
blagorost_task_development: { type: 'string' },
|
||||
return_source_description: { type: 'string' },
|
||||
return_additional_source: { type: 'string' },
|
||||
offer_template_number: { type: 'string' },
|
||||
},
|
||||
required: Cooperative.Registry.capitalProgramPrivateDataRequiredFields,
|
||||
additionalProperties: false,
|
||||
}
|
||||
|
||||
// Схема для сверки
|
||||
export const Schema: JSONSchemaType<Model> = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
|
||||
@@ -2,45 +2,15 @@ import type { JSONSchemaType } from 'ajv'
|
||||
import { Cooperative } from 'cooptypes'
|
||||
import type { ITemplate } from '../Interfaces'
|
||||
import { IMetaJSONSchema } from '../Schema/MetaSchema'
|
||||
import { CapitalProgramPrivateDataSchema } from '../Schema/CapitalProgramPrivateDataSchema'
|
||||
import { CommonUserSchema, CooperativeSchema, VarsSchema } from '../Schema'
|
||||
|
||||
export const registry_id = Cooperative.Registry.GeneratorOffer.registry_id
|
||||
|
||||
// Модель действия для генерации
|
||||
export type Action = Cooperative.Registry.GeneratorOffer.Action
|
||||
|
||||
// Модель данных
|
||||
export type Model = Cooperative.Registry.GeneratorOffer.Model
|
||||
|
||||
const CapitalProgramPrivateDataSchema: JSONSchemaType<Cooperative.Registry.CapitalProgramPrivateData> = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
approval_protocol_number: { type: 'string' },
|
||||
approval_protocol_day: { type: 'string' },
|
||||
approval_protocol_month: { type: 'string' },
|
||||
approval_protocol_year: { type: 'string' },
|
||||
cooperative_name: { type: 'string' },
|
||||
cooperative_short_name: { type: 'string' },
|
||||
cooperative_quoted_name: { type: 'string' },
|
||||
website: { type: 'string' },
|
||||
chairman_full_name: { type: 'string' },
|
||||
generator_program_purpose: { type: 'string' },
|
||||
eoap_definition: { type: 'string' },
|
||||
generator_task_goal: { type: 'string' },
|
||||
idea_unit_cost: { type: 'string' },
|
||||
idea_unit_cost_words: { type: 'string' },
|
||||
blagorost_goal_expansion: { type: 'string' },
|
||||
blagorost_task_expansion: { type: 'string' },
|
||||
blagorost_task_development: { type: 'string' },
|
||||
return_source_description: { type: 'string' },
|
||||
return_additional_source: { type: 'string' },
|
||||
offer_template_number: { type: 'string' },
|
||||
},
|
||||
required: Cooperative.Registry.capitalProgramPrivateDataRequiredFields,
|
||||
additionalProperties: false,
|
||||
}
|
||||
|
||||
// Схема для сверки
|
||||
export const Schema: JSONSchemaType<Model> = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
|
||||
@@ -1,60 +1,25 @@
|
||||
import type { JSONSchemaType } from 'ajv'
|
||||
import { Cooperative } from 'cooptypes'
|
||||
import type { IGenerate, IMetaDocument, ITemplate } from '../Interfaces'
|
||||
import type { IVars } from '../Models'
|
||||
import type { ITemplate } from '../Interfaces'
|
||||
import { IMetaJSONSchema } from '../Schema/MetaSchema'
|
||||
import { VarsSchema } from '../Schema'
|
||||
import { CapitalProgramPrivateDataSchema } from '../Schema/CapitalProgramPrivateDataSchema'
|
||||
import { CooperativeSchema, VarsSchema } from '../Schema'
|
||||
|
||||
export const registry_id = Cooperative.Registry.BlagorostProgramTemplate.registry_id
|
||||
|
||||
// Модель действия для генерации
|
||||
export interface Action extends IGenerate {
|
||||
registry_id: number
|
||||
}
|
||||
export type Action = Cooperative.Registry.BlagorostProgramTemplate.Action
|
||||
|
||||
// Модель данных
|
||||
export interface Model {
|
||||
meta: IMetaDocument
|
||||
vars: IVars
|
||||
}
|
||||
export type Model = Cooperative.Registry.BlagorostProgramTemplate.Model
|
||||
|
||||
const CapitalProgramPrivateDataSchema: JSONSchemaType<Cooperative.Registry.CapitalProgramPrivateData> = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
approval_protocol_number: { type: 'string' },
|
||||
approval_protocol_day: { type: 'string' },
|
||||
approval_protocol_month: { type: 'string' },
|
||||
approval_protocol_year: { type: 'string' },
|
||||
cooperative_name: { type: 'string' },
|
||||
cooperative_short_name: { type: 'string' },
|
||||
cooperative_quoted_name: { type: 'string' },
|
||||
website: { type: 'string' },
|
||||
chairman_full_name: { type: 'string' },
|
||||
generator_program_purpose: { type: 'string' },
|
||||
eoap_definition: { type: 'string' },
|
||||
generator_task_goal: { type: 'string' },
|
||||
idea_unit_cost: { type: 'string' },
|
||||
idea_unit_cost_words: { type: 'string' },
|
||||
blagorost_goal_expansion: { type: 'string' },
|
||||
blagorost_task_expansion: { type: 'string' },
|
||||
blagorost_task_development: { type: 'string' },
|
||||
return_source_description: { type: 'string' },
|
||||
return_additional_source: { type: 'string' },
|
||||
offer_template_number: { type: 'string' },
|
||||
},
|
||||
required: Cooperative.Registry.capitalProgramPrivateDataRequiredFields,
|
||||
additionalProperties: false,
|
||||
}
|
||||
|
||||
// Схема для сверки
|
||||
export const Schema: JSONSchemaType<Model> = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
meta: IMetaJSONSchema,
|
||||
coop: CooperativeSchema,
|
||||
vars: VarsSchema,
|
||||
doc_data: CapitalProgramPrivateDataSchema,
|
||||
},
|
||||
required: ['meta', 'vars', 'doc_data'],
|
||||
required: ['meta', 'coop', 'vars', 'doc_data'],
|
||||
additionalProperties: true,
|
||||
}
|
||||
|
||||
|
||||
@@ -2,45 +2,15 @@ import type { JSONSchemaType } from 'ajv'
|
||||
import { Cooperative } from 'cooptypes'
|
||||
import type { ITemplate } from '../Interfaces'
|
||||
import { IMetaJSONSchema } from '../Schema/MetaSchema'
|
||||
import { CapitalProgramPrivateDataSchema } from '../Schema/CapitalProgramPrivateDataSchema'
|
||||
import { CooperativeSchema, VarsSchema } from '../Schema'
|
||||
|
||||
export const registry_id = Cooperative.Registry.BlagorostOfferTemplate.registry_id
|
||||
|
||||
// Модель действия для генерации
|
||||
export type Action = Cooperative.Registry.BlagorostOfferTemplate.Action
|
||||
|
||||
// Модель данных
|
||||
export type Model = Cooperative.Registry.BlagorostOfferTemplate.Model
|
||||
|
||||
const CapitalProgramPrivateDataSchema: JSONSchemaType<Cooperative.Registry.CapitalProgramPrivateData> = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
approval_protocol_number: { type: 'string' },
|
||||
approval_protocol_day: { type: 'string' },
|
||||
approval_protocol_month: { type: 'string' },
|
||||
approval_protocol_year: { type: 'string' },
|
||||
cooperative_name: { type: 'string' },
|
||||
cooperative_short_name: { type: 'string' },
|
||||
cooperative_quoted_name: { type: 'string' },
|
||||
website: { type: 'string' },
|
||||
chairman_full_name: { type: 'string' },
|
||||
generator_program_purpose: { type: 'string' },
|
||||
eoap_definition: { type: 'string' },
|
||||
generator_task_goal: { type: 'string' },
|
||||
idea_unit_cost: { type: 'string' },
|
||||
idea_unit_cost_words: { type: 'string' },
|
||||
blagorost_goal_expansion: { type: 'string' },
|
||||
blagorost_task_expansion: { type: 'string' },
|
||||
blagorost_task_development: { type: 'string' },
|
||||
return_source_description: { type: 'string' },
|
||||
return_additional_source: { type: 'string' },
|
||||
offer_template_number: { type: 'string' },
|
||||
},
|
||||
required: Cooperative.Registry.capitalProgramPrivateDataRequiredFields,
|
||||
additionalProperties: false,
|
||||
}
|
||||
|
||||
// Схема для сверки
|
||||
export const Schema: JSONSchemaType<Model> = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
|
||||
@@ -48,27 +48,29 @@ beforeAll(async () => {
|
||||
await generator.save('udata', { ...commonUdata, ...data })
|
||||
}
|
||||
|
||||
const capitalProgramPrivateData = {
|
||||
approval_protocol_number: '1',
|
||||
approval_protocol_day: '15',
|
||||
approval_protocol_month: 'января',
|
||||
approval_protocol_year: '26',
|
||||
cooperative_name: 'ВОСХОД',
|
||||
cooperative_short_name: 'ПК',
|
||||
cooperative_quoted_name: '«ВОСХОД»',
|
||||
website: 'https://цифровой-кооператив.рф',
|
||||
chairman_full_name: 'Муравьев Алексей Николаевич',
|
||||
generator_program_purpose: 'развитию информационной экосистемы взаимодействия физических и юридических лиц, на основе международных кооперативных принципов и законодательства Российской Федерации в отношении потребительских обществ (кооперативов) под названием “Кооперативная Экономика”',
|
||||
eoap_definition: 'информационная экосистема, интегрируемая в социально-экономическую среду Российской Федерации, состоящая из комплекса программных продуктов на базе технологии распределенного реестра, обеспечивающих широкое экономическое и социальное взаимодействие физических и юридических лиц, включая нерезидентов различных юрисдикций и организационно-правовых форм, на основе международных кооперативных принципов и законодательства Российской Федерации в отношении потребительских кооперативов (обществ) под названием “Кооперативная Экономика”',
|
||||
generator_task_goal: 'центра привлечения и интеграции передовых инновационных цифровых разработок, а также экономических и социальных методов и решений',
|
||||
idea_unit_cost: '50',
|
||||
idea_unit_cost_words: 'пятьдесят',
|
||||
blagorost_goal_expansion: 'вследствие увеличения количества Участников информационной кооперативной экосистемы - ЕОАП - для расширения и повышения социальной эффективности их экономического взаимодействия в некоммерческом формате',
|
||||
blagorost_task_expansion: 'расширение участников ЕОАП - информационной кооперативной экосистемы как центра экономического взаимодействия в некоммерческом формате',
|
||||
blagorost_task_development: 'развитие ЕОАП как центра привлечения и интеграции инновационных цифровых разработок, а также экономических и социальных методов и решений',
|
||||
return_source_description: 'аппаратно-программная сеть узлов распределенного реестра в формате «СМЭВ+SWIFT», построенная на принципах самоорганизации и самофинансирования деятельности технологической инфраструктуры ЕОАП, обеспечивающей консенсус ее распределенных узлов по формированию базового продукта ЕОАП - полного цикла документооборота по синхронному взаимодействию пайщиков и кооперативов - участников экосистемы ЕОАП - через использование цифровых контрактов, с одновременным выполнением функций нотариата и учета финансовых и юридических событий',
|
||||
return_additional_source: 'взносы пользователей ЕОАП, его отдельных программных продуктов и приложений, переданных Обществу или создаваемых в рамках Общества, которые интегрируются в ЕОАП',
|
||||
offer_template_number: '______',
|
||||
const capitalProgramPrivateData = {
|
||||
generator_program_purpose:
|
||||
"развитию информационной экосистемы взаимодействия физических и юридических лиц, на основе международных кооперативных принципов и законодательства Российской Федерации в отношении потребительских обществ (кооперативов) – под названием “Кооперативная Экономика”, с целью самоорганизации и интеграции в социально-экономическую среду Российской Федерации., на основе международных кооперативных принципов и законодательства Российской Федерации",
|
||||
eoap_definition:
|
||||
"информационная экосистема, интегрируемая в социально-экономическую среду Российской Федерации, состоящая из комплекса программных продуктов на базе технологии распределенного реестра, обеспечивающих широкое экономическое и социальное взаимодействие физических и юридических лиц, включая нерезидентов различных юрисдикций и организационно-правовых форм, на основе международных кооперативных принципов и законодательства Российской Федерации в отношении потребительских кооперативов (обществ) под названием “Кооперативная Экономика”",
|
||||
generator_task_goal:
|
||||
"центра привлечения и интеграции передовых инновационных цифровых разработок, а также экономических и социальных методов и решений",
|
||||
idea_unit_cost:
|
||||
"50",
|
||||
idea_unit_cost_words:
|
||||
"пятьдесят",
|
||||
blagorost_goal_expansion:
|
||||
"развитии информационной экосистемы взаимодействия физических и юридических лиц, на основе международных кооперативных принципов и законодательства Российской Федерации в отношении потребительских обществ (кооперативов) – под названием “Кооперативная Экономика”, с целью самоорганизации и интеграции в социально-экономическую среду Российской Федерации",
|
||||
blagorost_goal_reason:
|
||||
"вследствие увеличения количества Участников информационной кооперативной экосистемы - ЕОАП - для расширения и повышения социальной эффективности их экономического взаимодействия в некоммерческом формате",
|
||||
blagorost_task_expansion:
|
||||
"расширение участников ЕОАП - информационной кооперативной экосистемы как центра экономического взаимодействия в некоммерческом формате",
|
||||
blagorost_task_development:
|
||||
"развитие ЕОАП как центра привлечения и интеграции инновационных цифровых разработок, а также экономических и социальных методов и решений",
|
||||
return_source_description:
|
||||
"аппаратно-программная сеть узлов распределенного реестра в формате «СМЭВ+SWIFT», построенная на принципах самоорганизации и самофинансирования деятельности технологической инфраструктуры ЕОАП, обеспечивающей консенсус ее распределенных узлов по формированию базового продукта ЕОАП - полного цикла документооборота по синхронному взаимодействию пайщиков и кооперативов - участников экосистемы ЕАОП - через использование цифровых контрактов, с одновременным выполнением функций нотариата и учета финансовых и юридических событий (подробнее на Сайте)",
|
||||
return_additional_source:
|
||||
"взносы пользователей ЕОАП, его отдельных программных продуктов и приложений, переданных Обществу или создаваемых в рамках Общества, которые интегрируются в ЕОАП"
|
||||
}
|
||||
|
||||
const { hash } = await generator.saveDocData(capitalProgramPrivateData, 994)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@coopenomics/inter",
|
||||
"type": "module",
|
||||
"version": "2026.6.24",
|
||||
"version": "2026.7.21",
|
||||
"description": "Контракты и DI-токены для связи расширений контроллера без прямых зависимостей",
|
||||
"author": "Alex Ant <dacom.dark.sun@gmail.com>",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"local": "049_migrate_phase2_progwallets.ts",
|
||||
"test": "051_edit_selected_draft.ts",
|
||||
"prod": "051_edit_selected_draft.ts"
|
||||
"test": "052_edit_selected_draft.ts",
|
||||
"prod": "052_edit_selected_draft.ts"
|
||||
}
|
||||
+1
-1
@@ -7,7 +7,7 @@ import { api } from '../src/eos'
|
||||
|
||||
export class InitialMigration implements Migration {
|
||||
async run(): Promise {
|
||||
const ids = [100]
|
||||
const ids = [994, 995, 996, 998, 999, 1000]
|
||||
|
||||
for (const id of ids) {
|
||||
try {
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "migrator",
|
||||
"version": "2026.6.24",
|
||||
"version": "2026.7.21",
|
||||
"description": "Утилита миграции данных между версиями",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@coopenomics/notifications",
|
||||
"version": "2026.6.24",
|
||||
"version": "2026.7.21",
|
||||
"description": "Каталог типобезопасных типов уведомлений и шаблонов Центра уведомлений",
|
||||
"type": "module",
|
||||
"private": false,
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "pkg-placeholder",
|
||||
"version": "2026.6.24",
|
||||
"version": "2026.7.21",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "pkg-placeholder",
|
||||
"version": "2026.6.24",
|
||||
"version": "2026.7.21",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@antfu/eslint-config": "^2.16.0",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@coopenomics/parser",
|
||||
"type": "module",
|
||||
"version": "2026.6.24",
|
||||
"version": "2026.7.21",
|
||||
"private": true,
|
||||
"packageManager": "pnpm@9.0.6",
|
||||
"description": "Индексатор блокчейна через State History Plugin",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@coopenomics/sdk",
|
||||
"type": "module",
|
||||
"version": "2026.6.24",
|
||||
"version": "2026.7.21",
|
||||
"private": false,
|
||||
"packageManager": "pnpm@9.9.0",
|
||||
"description": "TypeScript SDK для работы с API кооператива",
|
||||
|
||||
@@ -4,6 +4,7 @@ export * as CalculateVotes from './calculateVotes'
|
||||
export * as CloseProject from './closeProject'
|
||||
export * as CompleteCapitalRegistration from './completeCapitalRegistration'
|
||||
export * as CompleteOnboardingStep from './completeOnboardingStep'
|
||||
export * as SaveCapitalProgramDocDataHash from './saveCapitalProgramDocDataHash'
|
||||
export * as CompleteVoting from './completeVoting'
|
||||
export * as ConvertSegment from './convertSegment'
|
||||
export * as CreateCommit from './createCommit'
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import { capitalOnboardingStateSelector } from '../../selectors'
|
||||
import { $, type GraphQLTypes, type InputType, type ModelTypes, Selector } from '../../zeus'
|
||||
|
||||
export const name = 'saveCapitalProgramDocDataHash'
|
||||
|
||||
export const mutation = Selector('Mutation')({
|
||||
[name]: [{ data: $('data', 'SaveCapitalProgramDocDataInput!') }, capitalOnboardingStateSelector],
|
||||
})
|
||||
|
||||
export interface IInput {
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
[key: string]: unknown
|
||||
|
||||
data: ModelTypes['SaveCapitalProgramDocDataInput']
|
||||
}
|
||||
|
||||
export type IOutput = InputType<GraphQLTypes['Mutation'], typeof mutation>
|
||||
@@ -2,7 +2,7 @@ import type { GraphQLTypes, InputType, ModelTypes } from '../../zeus/index'
|
||||
import { rawPaymentMethodPaginationSelector } from '../../selectors/paymentMethods/paginatedPaymentMethodsSelector'
|
||||
import { $, Selector } from '../../zeus/index'
|
||||
|
||||
const name = 'getPaymentMethods'
|
||||
export const name = 'getPaymentMethods'
|
||||
|
||||
/**
|
||||
* Извлекает методы платежа
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user