Compare commits

...

5 Commits

Author SHA1 Message Date
Alex Ant 6a450e2ac6 some steps 2025-07-03 17:58:45 +05:00
Alex Ant 9fc87a705f bug: meet service improve 2025-07-02 13:27:31 +05:00
Alex Ant 305864c1ed chore(release): publish 2025-07-02 12:16:36 +05:00
Alex Ant ad83d9cc77 fix: meet сервис на отправку уведомления о повторном собрании 2025-07-02 11:58:51 +05:00
Alex Ant 226c7f9473 chore(release): publish 2025-07-01 21:03:01 +05:00
31 changed files with 656 additions and 287 deletions
+27
View File
@@ -1,3 +1,30 @@
# v2025.7.1-1
В этом релизе реализованы механизмы возврата паевого взноса пайщика и инструменты управления этим процессом для членов совета. Также внесены визуальные доработки интерфейсов для улучшения восприятия информации.
✨ Новые функции
- [#276](https://github.com/coopenomics/mono/issues/276): Реализовать путь возврата паевого взноса из кошелька пайщика
- [#281](https://github.com/coopenomics/mono/issues/281): Генерация заявления на возврат паевого взноса
- [#282](https://github.com/coopenomics/mono/issues/282): Генерация решения совета на возврат паевого взноса
- [#278](https://github.com/coopenomics/mono/issues/278): Введение методов управления возвратами паевых взносов в контроллере
- [#279](https://github.com/coopenomics/mono/issues/279): Отобразить исходящие платежи с кнопками управления в реестре платежей для совета
🐛 Исправления ошибок
- [#286](https://github.com/coopenomics/mono/issues/286): Поправить ошибки склонений времени
- [#262](https://github.com/coopenomics/mono/issues/262): Баг: рабочий стол совета включается, однако, страница всегда открывается со стола пайщика
- [#261](https://github.com/coopenomics/mono/issues/261): Баг: первая загрузка переадресует на главную страницу всегда - прямой переход на собрание становится недоступен.
🔧 Улучшения
- [#285](https://github.com/coopenomics/mono/issues/285): Корректировка дизайна кошелька, профиля, повестки совета, реестра документов, реестра платежей
- [#284](https://github.com/coopenomics/mono/issues/284): Разместить кошелек на главную вместо профиля
- [#283](https://github.com/coopenomics/mono/issues/283): Ввести лоадер на переходе между рабочими столами
- [#280](https://github.com/coopenomics/mono/issues/280): Мигрировать имеющиеся данные о входящих платежах в новую модель
- [#277](https://github.com/coopenomics/mono/issues/277): Рефакторинг модуля платежей: переход на унифицированную модель входящего и исходящего платежа
#releases
---
# v2025.6.14
Разработан модуль для проведения очередных общих собраний пайщиков. Исправлены баги, внесены улучшения пользовательского интерфейса.
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@coopenomics/boot",
"type": "module",
"version": "2025.7.1",
"version": "2025.7.2",
"private": true,
"packageManager": "pnpm@9.0.6",
"description": "",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@coopenomics/cleos",
"version": "2025.7.1",
"version": "2025.7.2",
"private": true,
"description": "",
"scripts": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@coopenomics/contracts",
"version": "2025.7.1",
"version": "2025.7.2",
"private": true,
"scripts": {
"build:all": "./build-all.sh",
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "create-nodejs-express-app",
"version": "2025.7.1",
"version": "2025.7.2",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "create-nodejs-express-app",
"version": "2025.7.1",
"version": "2025.7.2",
"license": "MIT",
"dependencies": {
"@a2seven/yoo-checkout": "^1.1.4",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@coopenomics/controller",
"version": "2025.7.1",
"version": "2025.7.2",
"description": "",
"private": false,
"bin": "bin/createNodejsApp.js",
@@ -50,6 +50,12 @@ export class MeetTrackerService {
// Основная функция проверки собраний
async checkMeets(): Promise<void> {
try {
// Подтягиваем актуальный конфиг из базы
const repo = await this.extensionRepository.findByName(this.extensionName);
if (repo) {
this.pluginConfig = repo;
}
// Получаем все собрания из блокчейна через порт
const meets = await this.meetPort.getMeets({ coopname: config.coopname }, undefined);
if (!meets || meets.length === 0) {
@@ -139,6 +145,8 @@ export class MeetTrackerService {
if (closedMeetIds.includes(meetID)) {
continue;
}
// Удаляем все старые собрания с тем же id перед добавлением нового
this.pluginConfig.config.trackedMeets = this.pluginConfig.config.trackedMeets.filter((tm) => tm.id !== meetID);
// Создаем новое отслеживаемое собрание
const newTrackedMeet: TrackedMeet = {
id: meetID,
@@ -207,6 +215,15 @@ export class MeetTrackerService {
if (statusChanged) {
this.logger.info(`Изменение статуса собрания ${meetHash} (№${meetID}): ${oldStatus} -> ${extendedStatus}`);
if (
oldStatus === ExtendedMeetStatus.ONRESTART &&
extendedStatus === ExtendedMeetStatus.WAITING_FOR_OPENING &&
!trackedMeet.notifications.restartNotification
) {
await this.notificationSender.sendRestartNotification(trackedMeet);
trackedMeet.notifications.restartNotification = true;
}
// При переходе с created на waitingForOpening отправляем начальное уведомление
if (
oldStatus === ExtendedMeetStatus.CREATED &&
@@ -0,0 +1,167 @@
import { Inject, Injectable, OnModuleDestroy } from '@nestjs/common';
import Redis from 'ioredis';
import { REDIS_PROVIDER } from './redis.provider';
import { WinstonLoggerService } from '~/modules/logger/logger-app.service';
export interface StreamMessage {
messageId: string;
fields: Record<string, string>;
}
export interface StreamConsumerOptions {
stream: string;
group: string;
consumer: string;
count?: number;
block?: number;
}
/**
* Сервис для работы с Redis Streams
* Обеспечивает надежное потребление событий от parser
*/
@Injectable()
export class RedisStreamService implements OnModuleDestroy {
private consumers: Map<string, boolean> = new Map();
constructor(
@Inject(REDIS_PROVIDER) private readonly redisClient: { subscriber: Redis; publisher: Redis },
private readonly logger: WinstonLoggerService
) {
this.logger.setContext(RedisStreamService.name);
}
onModuleDestroy() {
// Останавливаем всех потребителей
this.consumers.forEach((_, key) => {
this.consumers.set(key, false);
});
}
/**
* Создание группы потребителей
*/
async createConsumerGroup(stream: string, group: string, startId = '0'): Promise<void> {
try {
await this.redisClient.subscriber.xgroup('CREATE', stream, group, startId, 'MKSTREAM');
this.logger.log(`Consumer group created: ${group} for stream: ${stream}`);
} catch (error: any) {
if (error.message.includes('BUSYGROUP')) {
this.logger.log(`Consumer group ${group} already exists for stream ${stream}`);
} else {
this.logger.error(`Error creating consumer group: ${error.message}`);
throw error;
}
}
}
/**
* Потребление сообщений из stream
*/
async startConsumer(
options: StreamConsumerOptions,
messageHandler: (message: StreamMessage) => Promise<void>
): Promise<void> {
const { stream, group, consumer, count = 1, block = 1000 } = options;
const consumerKey = `${stream}:${group}:${consumer}`;
// Отмечаем потребителя как активного
this.consumers.set(consumerKey, true);
this.logger.log(`Starting consumer: ${consumerKey}`);
while (this.consumers.get(consumerKey)) {
try {
const result: any = await this.redisClient.subscriber.xreadgroup(
'GROUP',
group,
consumer,
'COUNT',
count.toString(),
'BLOCK',
block.toString(),
'STREAMS',
stream,
'>'
);
if (result && result.length > 0) {
for (const [streamName, messages] of result) {
if (streamName === stream) {
for (const [messageId, messageData] of messages) {
const fields: Record<string, string> = {};
// Преобразуем массив [key, value, key, value] в объект
for (let i = 0; i < messageData.length; i += 2) {
fields[messageData[i]] = messageData[i + 1];
}
try {
await messageHandler({ messageId, fields });
await this.acknowledgeMessage(stream, group, messageId);
} catch (error: any) {
this.logger.error(`Error processing message ${messageId}: ${error.message}`);
// Не подтверждаем сообщение при ошибке - оно останется в pending
}
}
}
}
}
} catch (error: any) {
if (this.consumers.get(consumerKey)) {
this.logger.error(`Consumer ${consumerKey} error: ${error.message}`);
// Небольшая пауза перед повторной попыткой
await new Promise((resolve) => setTimeout(resolve, 5000));
}
}
}
this.logger.log(`Consumer stopped: ${consumerKey}`);
}
/**
* Подтверждение обработки сообщения
*/
async acknowledgeMessage(stream: string, group: string, messageId: string): Promise<void> {
try {
await this.redisClient.subscriber.xack(stream, group, messageId);
this.logger.debug(`Message acknowledged: ${messageId} in stream ${stream}`);
} catch (error: any) {
this.logger.error(`Error acknowledging message ${messageId} in stream ${stream}, group ${group}: ${error.message}`);
throw error;
}
}
/**
* Остановка потребителя
*/
stopConsumer(stream: string, group: string, consumer: string): void {
const consumerKey = `${stream}:${group}:${consumer}`;
this.consumers.set(consumerKey, false);
this.logger.log(`Stopping consumer: ${consumerKey}`);
}
/**
* Получение информации о pending сообщениях
*/
async getPendingMessages(stream: string, group: string): Promise<any> {
try {
return await this.redisClient.subscriber.xpending(stream, group);
} catch (error: any) {
this.logger.error(`Error getting pending messages: ${error.message}`);
throw error;
}
}
/**
* Получение информации о группах потребителей
*/
async getConsumerGroups(stream: string): Promise<any> {
try {
return await this.redisClient.subscriber.xinfo('GROUPS', stream);
} catch (error: any) {
this.logger.error(`Error getting consumer groups: ${error.message}`);
throw error;
}
}
}
@@ -2,10 +2,11 @@
import { Module } from '@nestjs/common';
import { RedisService } from './redis.service';
import { RedisStreamService } from './redis-stream.service';
import { RedisProvider } from './redis.provider';
@Module({
providers: [RedisProvider, RedisService],
exports: [RedisService], // Экспортируем RedisService для использования в других модулях
providers: [RedisProvider, RedisService, RedisStreamService],
exports: [RedisService, RedisStreamService], // Экспортируем оба сервиса
})
export class RedisModule {}
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "cooptypes",
"type": "module",
"version": "2025.7.1",
"version": "2025.7.2",
"description": "_description_",
"author": "Alex Ant <dacom.dark.sun@gmail.com>",
"license": "MIT",
+2 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@coopenomics/desktop",
"version": "2025.7.1",
"version": "2025.7.2",
"description": "A Desktop Project",
"productName": "Desktop App",
"author": "Alex Ant <dacom.dark.sun@gmail.com>",
@@ -68,6 +68,7 @@
"qrcode": "^1.5.4",
"quasar": "^2.16.0",
"serialize-javascript": "^6.0.2",
"socket.io": "^4.8.1",
"swiper": "^11.1.7",
"vite": "2.9.16",
"vue": "^3.4.18",
+14 -16
View File
@@ -11,18 +11,19 @@
const { configure } = require('quasar/wrappers');
const path = require('path');
module.exports = configure(function (ctx) {
const isSSR = ctx.mode.ssr;
const isDev = ctx.dev;
// Загружаем переменные окружения всегда в режиме разработки
// или только для клиентской части в продакшн
const env = (isDev || !isSSR) ? require('dotenv').config().parsed : {
CLIENT: process.env.CLIENT,
SERVER: process.env.SERVER,
};
const env =
isDev || !isSSR
? require('dotenv').config().parsed
: {
CLIENT: process.env.CLIENT,
SERVER: process.env.SERVER,
};
return {
htmlVariables: {
@@ -39,10 +40,7 @@ module.exports = configure(function (ctx) {
// app boot file (/src/boot)
// --> boot files are part of "main.js"
// https://v2.quasar.dev/quasar-cli-vite/boot-files
boot: [
'init',
'i18n', 'axios', 'sentry'
],
boot: ['init', 'i18n', 'axios', 'sentry'],
// https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#css
css: [
@@ -118,14 +116,14 @@ module.exports = configure(function (ctx) {
eslint: {
lintCommand: 'eslint "./**/*.{js,ts,mjs,cjs,vue}"',
},
overlay: true
overlay: true,
},
{ server: false },
],
],
optimizeDeps: {
include: ['@dicebear/core', '@dicebear/collection']
include: ['@dicebear/core', '@dicebear/collection'],
},
},
@@ -135,10 +133,10 @@ module.exports = configure(function (ctx) {
vueDevtools: false,
open: false, // opens browser window automatically
port: 3005,
hmr:{
hmr: {
// clientPort: 3005,
// overlay: false
}
// overlay: false
},
},
// https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#framework
@@ -204,7 +202,7 @@ module.exports = configure(function (ctx) {
],
// Не обрабатывать эти модули для SSR
noExternal: ['@dicebear/core', '@dicebear/collection']
noExternal: ['@dicebear/core', '@dicebear/collection'],
},
// https://v2.quasar.dev/quasar-cli-vite/developing-pwa/configuring-pwa
+10
View File
@@ -0,0 +1,10 @@
/* eslint-disable */
// THIS FEATURE-FLAG FILE IS AUTOGENERATED,
// REMOVAL OR CHANGES WILL CAUSE RELATED TYPES TO STOP WORKING
import "quasar/dist/types/feature-flag";
declare module "quasar/dist/types/feature-flag" {
interface QuasarFeatureFlags {
pwa: true;
}
}
@@ -10,22 +10,22 @@ export default ssrMiddleware(({ app }) => {
app.use((req, res, next) => {
// Получаем переменные из process.env, которые нужны клиенту
const envForClient: EnvVars = {
NODE_ENV: process.env.NODE_ENV,
BACKEND_URL: process.env.BACKEND_URL,
CHAIN_URL: process.env.CHAIN_URL,
CHAIN_ID: process.env.CHAIN_ID,
CURRENCY: process.env.CURRENCY,
COOP_SHORT_NAME: process.env.COOP_SHORT_NAME,
SITE_DESCRIPTION: process.env.SITE_DESCRIPTION,
SITE_IMAGE: process.env.SITE_IMAGE,
STORAGE_URL: process.env.STORAGE_URL,
UPLOAD_URL: process.env.UPLOAD_URL,
NODE_ENV: process.env.NODE_ENV as string,
BACKEND_URL: process.env.BACKEND_URL as string,
CHAIN_URL: process.env.CHAIN_URL as string,
CHAIN_ID: process.env.CHAIN_ID as string,
CURRENCY: process.env.CURRENCY as string,
COOP_SHORT_NAME: process.env.COOP_SHORT_NAME as string,
SITE_DESCRIPTION: process.env.SITE_DESCRIPTION as string,
SITE_IMAGE: process.env.SITE_IMAGE as string,
STORAGE_URL: process.env.STORAGE_URL as string,
UPLOAD_URL: process.env.UPLOAD_URL as string,
TIMEZONE: process.env.TIMEZONE || 'Europe/Moscow',
VUE_ROUTER_MODE: process.env.VUE_ROUTER_MODE,
VUE_ROUTER_BASE: process.env.VUE_ROUTER_BASE,
NOVU_APP_ID: process.env.NOVU_APP_ID,
NOVU_BACKEND_URL: process.env.NOVU_BACKEND_URL,
NOVU_SOCKET_URL: process.env.NOVU_SOCKET_URL,
VUE_ROUTER_MODE: process.env.VUE_ROUTER_MODE as string,
VUE_ROUTER_BASE: process.env.VUE_ROUTER_BASE as string,
NOVU_APP_ID: process.env.NOVU_APP_ID as string,
NOVU_BACKEND_URL: process.env.NOVU_BACKEND_URL as string,
NOVU_SOCKET_URL: process.env.NOVU_SOCKET_URL as string,
};
// Создаем скрипт, который добавит переменные в window.__ENV__
@@ -1,23 +1,23 @@
// Типы для переменных окружения
export interface EnvVars {
NODE_ENV?: string;
BACKEND_URL?: string;
CHAIN_URL?: string;
CHAIN_ID?: string;
CURRENCY?: string;
COOP_SHORT_NAME?: string;
SITE_DESCRIPTION?: string;
SITE_IMAGE?: string;
STORAGE_URL?: string;
UPLOAD_URL?: string;
TIMEZONE?: string;
NODE_ENV: string;
BACKEND_URL: string;
CHAIN_URL: string;
CHAIN_ID: string;
CURRENCY: string;
COOP_SHORT_NAME: string;
SITE_DESCRIPTION: string;
SITE_IMAGE: string;
STORAGE_URL: string;
UPLOAD_URL: string;
TIMEZONE: string;
CLIENT?: boolean;
SERVER?: boolean;
VUE_ROUTER_MODE?: string;
VUE_ROUTER_BASE?: string;
NOVU_APP_ID?: string;
NOVU_BACKEND_URL?: string;
NOVU_SOCKET_URL?: string;
VUE_ROUTER_MODE: string;
VUE_ROUTER_BASE: string;
NOVU_APP_ID: string;
NOVU_BACKEND_URL: string;
NOVU_SOCKET_URL: string;
[key: string]: string | boolean | undefined;
}
@@ -41,24 +41,24 @@ function getEnv(): EnvVars {
// Для SSR сервера - берем реальные переменные
// Для SPA - эти значения заменятся при сборке
return {
NODE_ENV: process.env.NODE_ENV,
BACKEND_URL: process.env.BACKEND_URL,
CHAIN_URL: process.env.CHAIN_URL,
CHAIN_ID: process.env.CHAIN_ID,
CURRENCY: process.env.CURRENCY,
COOP_SHORT_NAME: process.env.COOP_SHORT_NAME,
SITE_DESCRIPTION: process.env.SITE_DESCRIPTION,
SITE_IMAGE: process.env.SITE_IMAGE,
STORAGE_URL: process.env.STORAGE_URL,
UPLOAD_URL: process.env.UPLOAD_URL,
NODE_ENV: process.env.NODE_ENV as string,
BACKEND_URL: process.env.BACKEND_URL as string,
CHAIN_URL: process.env.CHAIN_URL as string,
CHAIN_ID: process.env.CHAIN_ID as string,
CURRENCY: process.env.CURRENCY as string,
COOP_SHORT_NAME: process.env.COOP_SHORT_NAME as string,
SITE_DESCRIPTION: process.env.SITE_DESCRIPTION as string,
SITE_IMAGE: process.env.SITE_IMAGE as string,
STORAGE_URL: process.env.STORAGE_URL as string,
UPLOAD_URL: process.env.UPLOAD_URL as string,
TIMEZONE: process.env.TIMEZONE || 'Europe/Moscow',
CLIENT: process.env.CLIENT as unknown as boolean,
SERVER: process.env.SERVER as unknown as boolean,
VUE_ROUTER_MODE: process.env.VUE_ROUTER_MODE,
VUE_ROUTER_BASE: process.env.VUE_ROUTER_BASE,
NOVU_APP_ID: process.env.NOVU_APP_ID,
NOVU_BACKEND_URL: process.env.NOVU_BACKEND_URL,
NOVU_SOCKET_URL: process.env.NOVU_SOCKET_URL,
VUE_ROUTER_MODE: process.env.VUE_ROUTER_MODE as string,
VUE_ROUTER_BASE: process.env.VUE_ROUTER_BASE as string,
NOVU_APP_ID: process.env.NOVU_APP_ID as string,
NOVU_BACKEND_URL: process.env.NOVU_BACKEND_URL as string,
NOVU_SOCKET_URL: process.env.NOVU_SOCKET_URL as string,
};
}
@@ -1,130 +1,228 @@
<template lang="pug">
div(v-if="userData.organization_data").q-gutter-sm.q-mt-md
q-select(
v-model="userData.organization_data.type"
label="Выберите тип организации"
standout="bg-teal text-white"
:options="[{ label: 'Потребительский Кооператив', value: 'coop' }, { label: 'Производственный Кооператив', value: 'prodcoop' }, { label: 'ООО', value: 'ooo' }]"
emit-value
map-options).q-mb-md
q-input(v-model="userData.organization_data.short_name" standout="bg-teal text-white" hint="ПК Ромашка" label="Краткое наименование организации" :rules="[val => notEmpty(val)]" autocomplete="off")
q-input(v-model="userData.organization_data.full_name" standout="bg-teal text-white" hint="Потребительский Кооператив 'Ромашка'" label="Полное наименование организации" :rules="[val => notEmpty(val)]" autocomplete="off")
q-input(v-model="userData.organization_data.represented_by.last_name" standout="bg-teal text-white" label="Фамилия представителя" hint="" :rules="[val => notEmpty(val), val => validatePersonalName(val)]" autocomplete="off")
q-input(v-model="userData.organization_data.represented_by.first_name" standout="bg-teal text-white" label="Имя представителя" hint="" :rules="[val => notEmpty(val), val => validatePersonalName(val)]" autocomplete="off")
q-input(v-model="userData.organization_data.represented_by.middle_name" standout="bg-teal text-white" label="Отчество представителя" hint="" :rules="[val => validatePersonalName(val)]" autocomplete="off")
q-input(v-model="userData.organization_data.represented_by.based_on" standout="bg-teal text-white" label="Представитель действует на основании" hint="решения общего собрания №102 от 01.01.2025 г" :rules="[val => notEmpty(val)]" autocomplete="off")
q-input(v-model="userData.organization_data.represented_by.position" standout="bg-teal text-white" label="Должность представителя" hint="председатель совета" :rules="[val => notEmpty(val)]" autocomplete="off")
q-input(v-model="userData.organization_data.phone" standout="bg-teal text-white" label="Номер телефона представителя" mask="+7 (###) ###-##-##" fill-mask hint="" :rules="[val => notEmpty(val), val => notEmptyPhone(val)]" autocomplete="off")
q-select(v-model="userData.organization_data.country" standout="bg-teal text-white" map-options emit-value option-label="label" option-value="value" label="Страна" :options="[{ label: 'Россия', value: 'Russia' }]" :rules="[val => notEmpty(val)]" autocomplete="off")
q-input(v-model="userData.organization_data.city" standout="bg-teal text-white" label="Город" hint="" :rules="[val => notEmpty(val)]" autocomplete="off")
q-input(v-model="userData.organization_data.full_address" standout="bg-teal text-white" hint="" label="Юридический адрес регистрации" :rules="[val => notEmpty(val)]" autocomplete="off")
q-input(v-model="userData.organization_data.fact_address" standout="bg-teal text-white" hint="" label="Фактический адрес" :rules="[val => notEmpty(val)]" autocomplete="off")
template(v-slot:append)
q-btn(dense size="sm" color="teal" @click="userData.organization_data.fact_address = userData.organization_data.full_address") совпадает
q-input(
v-model="userData.organization_data.details.inn"
standout="bg-teal text-white"
mask="############"
label="ИНН организации"
hint="10 или 12 цифр"
:rules="[val => notEmpty(val), val => (val.length === 10 || val.length === 12) || 'ИНН должен содержать 10 или 12 цифр']"
autocomplete="off"
)
q-input(
v-model="userData.organization_data.details.ogrn"
standout="bg-teal text-white"
mask="###############"
label="ОГРН организации"
hint="13 или 15 цифр"
:rules="[val => notEmpty(val), val => (val.length === 13 || val.length === 15) || 'ОГРН должен содержать 13 или 15 цифр']"
autocomplete="off"
)
q-input(
v-model="userData.organization_data.details.kpp"
standout="bg-teal text-white"
mask="#########"
label="КПП организации"
hint="9 цифр"
:rules="[val => notEmpty(val), val => val.length === 9 || 'КПП должен содержать 9 цифр']"
autocomplete="off"
)
q-input(
v-model="userData.organization_data.bank_account.bank_name"
standout="bg-teal text-white"
label="Наименование банка"
hint="ПАО Сбербанк"
:rules="[val => notEmpty(val)]"
autocomplete="off"
)
q-input(
v-model="userData.organization_data.bank_account.details.corr"
standout="bg-teal text-white"
mask="####################"
label="Корреспондентский счет"
hint="20 цифр"
:rules="[val => notEmpty(val), val => val.length === 20 || 'Корреспондентский счет должен содержать 20 цифр']"
autocomplete="off"
)
q-input(
v-model="userData.organization_data.bank_account.details.bik"
standout="bg-teal text-white"
mask="#########"
label="БИК"
hint="9 цифр"
:rules="[val => notEmpty(val), val => val.length === 9 || 'БИК должен содержать 9 цифр']"
autocomplete="off"
)
q-input(
v-model="userData.organization_data.bank_account.details.kpp"
standout="bg-teal text-white"
mask="#########"
label="КПП (банка)"
hint="9 цифр"
:rules="[val => notEmpty(val), val => val.length === 9 || 'КПП должен содержать 9 цифр']"
autocomplete="off"
)
q-input(
v-model="userData.organization_data.bank_account.account_number"
standout="bg-teal text-white"
mask="####################"
label="Номер счета"
hint="20 цифр"
:rules="[val => notEmpty(val), val => val.length === 20 || 'Номер счета должен содержать 20 цифр']"
autocomplete="off"
)
q-select(
v-model="userData.organization_data.bank_account.currency"
label="Валюта счёта"
standout="bg-teal text-white"
:options="[{ label: 'RUB', value: 'RUB' }]"
emit-value
:rules="[val => notEmpty(val)]"
.q-gutter-sm.q-mt-md(v-if='userData.organization_data')
q-select.q-mb-md(
v-model='userData.organization_data.type',
label='Выберите тип организации',
standout='bg-teal text-white',
:options='[ { label: "Потребительский Кооператив", value: Zeus.OrganizationType.COOP }, { label: "Производственный Кооператив", value: Zeus.OrganizationType.PRODCOOP }, { label: "ООО", value: Zeus.OrganizationType.OOO }, ]',
emit-value,
map-options
)
q-input(
v-model='userData.organization_data.short_name',
standout='bg-teal text-white',
hint='ПК Ромашка',
label='Краткое наименование организации',
:rules='[(val) => notEmpty(val)]',
autocomplete='off'
)
q-input(
v-model='userData.organization_data.full_name',
standout='bg-teal text-white',
hint='Потребительский Кооператив \'Ромашка\'',
label='Полное наименование организации',
:rules='[(val) => notEmpty(val)]',
autocomplete='off'
)
q-input(
v-model='userData.organization_data.represented_by.last_name',
standout='bg-teal text-white',
label='Фамилия представителя',
hint='',
:rules='[(val) => notEmpty(val), (val) => validatePersonalName(val)]',
autocomplete='off'
)
q-input(
v-model='userData.organization_data.represented_by.first_name',
standout='bg-teal text-white',
label='Имя представителя',
hint='',
:rules='[(val) => notEmpty(val), (val) => validatePersonalName(val)]',
autocomplete='off'
)
q-input(
v-model='userData.organization_data.represented_by.middle_name',
standout='bg-teal text-white',
label='Отчество представителя',
hint='',
:rules='[(val) => validatePersonalName(val)]',
autocomplete='off'
)
q-input(
v-model='userData.organization_data.represented_by.based_on',
standout='bg-teal text-white',
label='Представитель действует на основании',
hint='решения общего собрания №102 от 01.01.2025 г',
:rules='[(val) => notEmpty(val)]',
autocomplete='off'
)
q-input(
v-model='userData.organization_data.represented_by.position',
standout='bg-teal text-white',
label='Должность представителя',
hint='председатель совета',
:rules='[(val) => notEmpty(val)]',
autocomplete='off'
)
q-input(
v-model='userData.organization_data.phone',
standout='bg-teal text-white',
label='Номер телефона представителя',
mask='+7 (###) ###-##-##',
fill-mask,
hint='',
:rules='[(val) => notEmpty(val), (val) => notEmptyPhone(val)]',
autocomplete='off'
)
q-select(
v-model='userData.organization_data.country',
standout='bg-teal text-white',
map-options,
emit-value,
option-label='label',
option-value='value',
label='Страна',
:options='[{ label: "Россия", value: "Russia" }]',
:rules='[(val) => notEmpty(val)]',
autocomplete='off'
)
q-input(
v-model='userData.organization_data.city',
standout='bg-teal text-white',
label='Город',
hint='',
:rules='[(val) => notEmpty(val)]',
autocomplete='off'
)
q-input(
v-model='userData.organization_data.full_address',
standout='bg-teal text-white',
hint='',
label='Юридический адрес регистрации',
:rules='[(val) => notEmpty(val)]',
autocomplete='off'
)
q-input(
v-model='userData.organization_data.fact_address',
standout='bg-teal text-white',
hint='',
label='Фактический адрес',
:rules='[(val) => notEmpty(val)]',
autocomplete='off'
)
template(v-slot:append)
q-btn(
dense,
size='sm',
color='teal',
@click='userData.organization_data.fact_address = userData.organization_data.full_address'
) совпадает
q-input(
v-model='userData.organization_data.details.inn',
standout='bg-teal text-white',
mask='############',
label='ИНН организации',
hint='10 или 12 цифр',
:rules='[(val) => notEmpty(val), (val) => val.length === 10 || val.length === 12 || "ИНН должен содержать 10 или 12 цифр"]',
autocomplete='off'
)
q-input(
v-model='userData.organization_data.details.ogrn',
standout='bg-teal text-white',
mask='###############',
label='ОГРН организации',
hint='13 или 15 цифр',
:rules='[(val) => notEmpty(val), (val) => val.length === 13 || val.length === 15 || "ОГРН должен содержать 13 или 15 цифр"]',
autocomplete='off'
)
q-input(
v-model='userData.organization_data.details.kpp',
standout='bg-teal text-white',
mask='#########',
label='КПП организации',
hint='9 цифр',
:rules='[(val) => notEmpty(val), (val) => val.length === 9 || "КПП должен содержать 9 цифр"]',
autocomplete='off'
)
q-input(
v-model='userData.organization_data.bank_account.bank_name',
standout='bg-teal text-white',
label='Наименование банка',
hint='ПАО Сбербанк',
:rules='[(val) => notEmpty(val)]',
autocomplete='off'
)
q-input(
v-model='userData.organization_data.bank_account.details.corr',
standout='bg-teal text-white',
mask='####################',
label='Корреспондентский счет',
hint='20 цифр',
:rules='[(val) => notEmpty(val), (val) => val.length === 20 || "Корреспондентский счет должен содержать 20 цифр"]',
autocomplete='off'
)
q-input(
v-model='userData.organization_data.bank_account.details.bik',
standout='bg-teal text-white',
mask='#########',
label='БИК',
hint='9 цифр',
:rules='[(val) => notEmpty(val), (val) => val.length === 9 || "БИК должен содержать 9 цифр"]',
autocomplete='off'
)
q-input(
v-model='userData.organization_data.bank_account.details.kpp',
standout='bg-teal text-white',
mask='#########',
label='КПП (банка)',
hint='9 цифр',
:rules='[(val) => notEmpty(val), (val) => val.length === 9 || "КПП должен содержать 9 цифр"]',
autocomplete='off'
)
q-input(
v-model='userData.organization_data.bank_account.account_number',
standout='bg-teal text-white',
mask='####################',
label='Номер счета',
hint='20 цифр',
:rules='[(val) => notEmpty(val), (val) => val.length === 20 || "Номер счета должен содержать 20 цифр"]',
autocomplete='off'
)
q-select(
v-model='userData.organization_data.bank_account.currency',
label='Валюта счёта',
standout='bg-teal text-white',
:options='[{ label: "RUB", value: "RUB" }]',
emit-value,
:rules='[(val) => notEmpty(val)]',
map-options
)
</template>
<script setup lang="ts">
import { validatePersonalName, notEmpty, notEmptyPhone } from 'src/shared/lib/utils';
import {
validatePersonalName,
notEmpty,
notEmptyPhone,
} from 'src/shared/lib/utils';
import type { IUserData } from 'src/shared/lib/types/user/IUserData';
import { ref } from 'vue';
import { Zeus } from '@coopenomics/sdk';
const props = defineProps<{ userData: IUserData }>();
const userData = ref<IUserData>(props.userData)
const userData = ref<IUserData>(props.userData);
</script>
@@ -10,7 +10,15 @@ q-header.header(bordered, :class='headerClass')
@click='emitToggleLeftDrawer'
)
BackButton(v-if='loggedIn')
q-toolbar-title
q-btn(
v-if='!loggedIn && coopTitle',
stretch,
flat,
@click='emitToggleLeftDrawer',
:size='isMobile ? "sm" : "lg"'
) {{ coopTitle }}
// Добавляем компонент уведомлений, если пользователь авторизован
NotificationCenter(v-if='loggedIn && isClient')
@@ -56,7 +64,6 @@ import { BackButton } from 'src/widgets/Header/BackButton';
import { NotificationCenter } from 'src/widgets/NotificationCenter';
import './HeaderStyles.scss';
import { useSystemStore } from 'src/entities/System/model';
// import { env } from 'src/shared/config'
const router = useRouter();
const route = useRoute();
@@ -66,12 +73,11 @@ const session = useSessionStore();
const { isMobile } = useWindowSize();
const emit = defineEmits(['toggle-left-drawer']);
const isClient = computed(
() => typeof process !== 'undefined' && process.env.CLIENT,
);
// Получаем информацию для навигации назад
// const coopTitle = computed(() => env.COOP_SHORT_NAME)
const coopTitle = computed(() => info.vars?.name);
const isClient = computed(() => process.env.CLIENT);
const isDark = computed(() => $q.dark.isActive);
const headerClass = computed(() =>
@@ -1,35 +1,44 @@
<template lang="pug">
div
</template>
<!-- NotificationCenter.vue
<template lang="pug">
div#notification-inbox.notification-container
#notification-inbox.notification-container
</template>
<script setup lang="ts">
import { computed, onMounted, watch } from 'vue'
import { NovuUI } from '@novu/js/ui'
import { dark } from '@novu/js/themes'
import { useCurrentUser } from 'src/entities/Session'
import { useQuasar } from 'quasar'
import { env } from 'src/shared/config'
const currentUser = useCurrentUser()
import { useCurrentUser } from 'src/entities/Session';
import { useQuasar } from 'quasar';
import { env } from 'src/shared/config';
import { computed, onMounted, watch } from 'vue';
import { useSystemStore } from 'src/entities/System/model';
const $q = useQuasar()
const isDark = computed(() => $q.dark.isActive)
let novu: any = null
const currentUser = useCurrentUser();
const $q = useQuasar();
const isDark = computed(() => $q.dark.isActive);
const { info } = useSystemStore();
function mountNovu() {
const subscriberId = currentUser.username || '68481006a874d6592b28c530'
const el = document.getElementById('notification-inbox')
let novu: any = null;
if (el) el.innerHTML = ''
async function mountNovu() {
// Динамический импорт только на клиенте
const { NovuUI } = await import('@novu/js/ui');
const { dark } = await import('@novu/js/themes');
const subscriberId = `${info.coopname}-${currentUser.username}`;
const el = document.getElementById('notification-inbox');
console.log('novuOptions, ', {
applicationIdentifier: env.NOVU_APP_ID,
subscriberId,
// Теперь используем наш бэкенд в качестве прокси
backendUrl: `${env.BACKEND_URL}/notifications`,
socketUrl: `${env.BACKEND_URL}/notifications`,
});
if (el) el.innerHTML = '';
novu = new NovuUI({
options: {
applicationIdentifier: env.NOVU_APP_ID,
subscriberId,
backendUrl: env.NOVU_BACKEND_URL,
socketUrl: env.NOVU_SOCKET_URL,
// Используем наш бэкенд в качестве прокси
backendUrl: `${env.BACKEND_URL}/notifications`,
socketUrl: `${env.BACKEND_URL}/notifications`,
},
appearance: {
baseTheme: isDark.value ? dark : undefined,
@@ -45,7 +54,8 @@ function mountNovu() {
'notifications.actions.readAll': 'Прочитать все',
'notifications.actions.archiveAll': 'Архивировать все',
'notifications.actions.archiveRead': 'Архивировать прочитанные',
'notifications.newNotifications': '{{notificationCount}} новое уведомление(ий)',
'notifications.newNotifications':
'{{notificationCount}} новое уведомление(ий)',
'notification.actions.read.tooltip': 'Отметить как прочитанное',
'notification.actions.unread.tooltip': 'Отметить как непрочитанное',
'notification.actions.archive.tooltip': 'В архив',
@@ -53,28 +63,32 @@ function mountNovu() {
'preferences.title': 'Настройки',
'preferences.emptyNotice': 'Пока нет настроек уведомлений.',
'preferences.global': 'Общие настройки',
'preferences.workflow.disabled.notice': 'Обратитесь к администратору для управления подпиской на это важное уведомление.',
'preferences.workflow.disabled.tooltip': 'Для изменения обратитесь к администратору',
'locale': 'ru-RU',
'dynamic': {
'comment-on-post': 'Комментарии к записи'
}
}
})
'preferences.workflow.disabled.notice':
'Обратитесь к администратору для управления подпиской на это важное уведомление.',
'preferences.workflow.disabled.tooltip':
'Для изменения обратитесь к администратору',
locale: 'ru-RU',
dynamic: {
'comment-on-post': 'Комментарии к записи',
},
},
});
novu.mountComponent({
name: 'Inbox',
props: {},
element: el as HTMLDivElement,
})
});
}
onMounted(() => {
mountNovu()
})
if (process.env.CLIENT) {
mountNovu();
}
});
watch(isDark, () => {
mountNovu()
})
mountNovu();
});
</script>
<style scoped>
@@ -89,4 +103,4 @@ watch(isDark, () => {
height: 24px;
width: 24px;
}
</style> -->
</style>
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@coopenomics/docs",
"type": "module",
"version": "2025.7.1",
"version": "2025.7.2",
"private": true,
"packageManager": "pnpm@9.9.0",
"description": "Документация для монорепозитория",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@coopenomics/factory",
"type": "module",
"version": "2025.7.1",
"version": "2025.7.2",
"packageManager": "pnpm@9.0.6",
"description": "",
"author": "Alex Ant <chairman.voskhod@gmail.com>",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "migrator",
"version": "2025.7.1",
"version": "2025.7.2",
"description": "",
"private": true,
"type": "module",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "coop-notificator",
"type": "module",
"version": "2025.7.1",
"version": "2025.7.2",
"private": true,
"packageManager": "pnpm@9.1.1",
"description": "_description_",
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "pkg-placeholder",
"version": "2025.7.1",
"version": "2025.7.2",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "pkg-placeholder",
"version": "2025.7.1",
"version": "2025.7.2",
"license": "MIT",
"devDependencies": {
"@antfu/eslint-config": "^2.16.0",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@coopenomics/parser",
"type": "module",
"version": "2025.7.1",
"version": "2025.7.2",
"private": false,
"packageManager": "pnpm@9.0.6",
"description": "",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@coopenomics/sdk",
"type": "module",
"version": "2025.7.1",
"version": "2025.7.2",
"private": false,
"packageManager": "pnpm@9.9.0",
"description": "",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@coopenomics/setup",
"version": "2025.7.1",
"version": "2025.7.2",
"private": true,
"description": "",
"main": "index.js",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"$schema": "node_modules/lerna/schemas/lerna-schema.json",
"version": "2025.7.1",
"version": "2025.7.2",
"packages": [
"components/*",
"components/controller/src/plugins/*"
+1
View File
@@ -10214,6 +10214,7 @@ packages:
lodash.get@4.4.2:
resolution: {integrity: sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==}
deprecated: This package is deprecated. Use the optional chaining (?.) operator instead.
lodash.includes@4.3.0:
resolution: {integrity: sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==}
+59 -24
View File
@@ -1,7 +1,7 @@
Сгенерируй changelog для версии v2025.6.14.
Сгенерируй changelog для версии v2025.7.1-1.
Правила:
- Начинай changelog заголовком: "# v2025.6.14"
- Начинай changelog заголовком: "# v2025.7.1-1"
- Затем добавь краткое текстовое описание релиза (1–3 строки), основанное на комментарии разработчика. Не копируй комментарий дословно. Используй его только как ориентир для формулировки описания.
- Не добавляй ничего от себя, не используй мотивационные или маркетинговые фразы.
- После описания сгруппируй изменения по категориям:
@@ -14,10 +14,65 @@
- В конце поставь тег: #releases
Комментарий разработчика для описания релиза:
Реализован модуль проведения очередных общих собраний пайщиков.
Релиз содержит изменения, реализующие функциональность возврата паевого взноса пайщика из цифрового кошелька и методы управления этим процессом для членов совета. Также, релиз содержит визуальные изменения в интерфейсы, направленные на улучшение визуального восприятия информации.
Ниже список закрытых issues, начиная с 2025-05-14:
Ниже список закрытых issues, начиная с 2025-05-22T10:00:00+03:00:
#286: Поправить ошибки склонений времени
---
#285: Корректировка дизайна кошелька, профиля, повестки совета, реестра документов, реестра платежей
---
#284: Разместить кошелек на главную вместо профиля
---
#283: Ввести лоадер на переходе между рабочими столами
---
#282: Генерация решения совета на возврат паевого взноса
---
#281: Генерация заявления на возврат паевого взноса
---
#280: Мигрировать имеющиеся данные о входящих платежах в новую модель
---
#279: Отобразить исходящие платежи с кнопками управления в реестре платежей для совета
---
#278: Введение методов управления возвратами паевых взносов в контроллере
---
#277: Рефакторинг модуля платежей: переход на унифицированную модель входящего и исходящего платежа
---
#276: Реализовать путь возврата паевого взноса из кошелька пайщика
---
#274: Настроить рассылку оповещений на почту при получении решения о проведении собрания
@@ -92,24 +147,4 @@
---
#259: Поправить отступы в мобильной карточке пайщика на странице пайщиков
---
#258: Убрать hover-эффект при наведении на документ или пайщика в таблице
---
#256: Отобразить статус членства каждого пайщика в разделе Пайщики
---
#255: Отобразить ФИО/Наименование плательщика в разделе Платежи
---
+15 -21
View File
@@ -1,30 +1,24 @@
# v2025.6.14
# v2025.7.1-1
Разработан модуль для проведения очередных общих собраний пайщиков. Исправлены баги, внесены улучшения пользовательского интерфейса.
В этом релизе реализованы механизмы возврата паевого взноса пайщика и инструменты управления этим процессом для членов совета. Также внесены визуальные доработки интерфейсов для улучшения восприятия информации.
✨ Новые функции
- [#264](https://github.com/coopenomics/mono/issues/264): Разработан смарт-контракт общего собрания пайщиков (meet)
- [#263](https://github.com/coopenomics/mono/issues/263): Реализован модуль оповещений на электронные почты по жизненному циклу общего собрания пайщиков
- [#273](https://github.com/coopenomics/mono/issues/273): Встроены реальные шаблоны документов общего собрания
- [#268](https://github.com/coopenomics/mono/issues/268): Добавлена страница результатов общего собрания
- [#267](https://github.com/coopenomics/mono/issues/267): Добавлена страница просмотра и скачивания бюллетеней и уведомлений по собранию
- [#270](https://github.com/coopenomics/mono/issues/270): Ссылка в оповещении ведет на страницу собрания с документом-уведомлением для подписи
- [#276](https://github.com/coopenomics/mono/issues/276): Реализовать путь возврата паевого взноса из кошелька пайщика
- [#281](https://github.com/coopenomics/mono/issues/281): Генерация заявления на возврат паевого взноса
- [#282](https://github.com/coopenomics/mono/issues/282): Генерация решения совета на возврат паевого взноса
- [#278](https://github.com/coopenomics/mono/issues/278): Введение методов управления возвратами паевых взносов в контроллере
- [#279](https://github.com/coopenomics/mono/issues/279): Отобразить исходящие платежи с кнопками управления в реестре платежей для совета
🐛 Исправления ошибок
- [#262](https://github.com/coopenomics/mono/issues/262): Исправлено некорректное открытие рабочего стола совета
- [#261](https://github.com/coopenomics/mono/issues/261): Исправлена ошибка с редиректом при первой загрузке и прямом переходе на собрание
- [#259](https://github.com/coopenomics/mono/issues/259): Исправлены отступы в мобильной карточке пайщика
- [#258](https://github.com/coopenomics/mono/issues/258): Убран hover-эффект на документе и пайщике в таблице
- [#286](https://github.com/coopenomics/mono/issues/286): Поправить ошибки склонений времени
- [#262](https://github.com/coopenomics/mono/issues/262): Баг: рабочий стол совета включается, однако, страница всегда открывается со стола пайщика
- [#261](https://github.com/coopenomics/mono/issues/261): Баг: первая загрузка переадресует на главную страницу всегда - прямой переход на собрание становится недоступен.
🔧 Улучшения
- [#274](https://github.com/coopenomics/mono/issues/274): Настроена рассылка оповещений на почту при получении решения о проведении собрания
- [#272](https://github.com/coopenomics/mono/issues/272): Подписанные уведомления сохраняются и извлекаются из реестра по Graph-QL
- [#271](https://github.com/coopenomics/mono/issues/271): Ведется подсчет количества пайщиков в каждом кооперативе при добавлении и удалении
- [#260](https://github.com/coopenomics/mono/issues/260): Введен счетчик количества пайщиков в кооперативе на контракте регистратора
- [#256](https://github.com/coopenomics/mono/issues/256): Отображается статус членства каждого пайщика
- [#255](https://github.com/coopenomics/mono/issues/255): В разделе Платежи отображается ФИО/Наименование плательщика
- [#269](https://github.com/coopenomics/mono/issues/269): Реализована рассылка уведомлений перед началом собрания
- [#265](https://github.com/coopenomics/mono/issues/265): Проведена отладка и тестирование процесса общего собрания пайщиков
- [#266](https://github.com/coopenomics/mono/issues/266): Протестированы все процессы общего собрания
- [#285](https://github.com/coopenomics/mono/issues/285): Корректировка дизайна кошелька, профиля, повестки совета, реестра документов, реестра платежей
- [#284](https://github.com/coopenomics/mono/issues/284): Разместить кошелек на главную вместо профиля
- [#283](https://github.com/coopenomics/mono/issues/283): Ввести лоадер на переходе между рабочими столами
- [#280](https://github.com/coopenomics/mono/issues/280): Мигрировать имеющиеся данные о входящих платежах в новую модель
- [#277](https://github.com/coopenomics/mono/issues/277): Рефакторинг модуля платежей: переход на унифицированную модель входящего и исходящего платежа
#releases
+3 -3
View File
@@ -1,4 +1,4 @@
VERSION: v2025.6.14
FROM DATE: 2025-05-14T10:00:00+03:00
VERSION: v2025.7.1-1
FROM DATE: 2025-05-22T10:00:00+03:00
COMMENT:
Реализован модуль проведения очередных общих собраний пайщиков.
Релиз содержит изменения, реализующие функциональность возврата паевого взноса пайщика из цифрового кошелька и методы управления этим процессом для членов совета. Также, релиз содержит визуальные изменения в интерфейсы, направленные на улучшение визуального восприятия информации.