Compare commits
214 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6ee7eb93df | |||
| 1f15f368a6 | |||
| f72b4900ab | |||
| f859651038 | |||
| d763f08fec | |||
| 805ac73ed7 | |||
| b82b778b81 | |||
| bcb76b4cd9 | |||
| a267a8a24b | |||
| 8135ea46a5 | |||
| 50e60f5750 | |||
| 92bd6919d8 | |||
| 63fdb74db5 | |||
| 5d837f2c20 | |||
| 426a06a076 | |||
| c2bb718895 | |||
| 04cf9dc3c4 | |||
| 5dec964a83 | |||
| dd2ebc2983 | |||
| 3f6c2538fc | |||
| 81cb16eb61 | |||
| 5de5e07dae | |||
| 5470e9ad89 | |||
| 8ac3f06928 | |||
| 6c1b6262b2 | |||
| 618349ce91 | |||
| 63d95f429d | |||
| 39a9727a93 | |||
| 1841c3ebaa | |||
| 46617a1430 | |||
| 4c4c611a3d | |||
| a42210e5b8 | |||
| 7e4cb15207 | |||
| 0759793fb0 | |||
| 1b9fbabac5 | |||
| 717eae8540 | |||
| c456602763 | |||
| a36f5a63a9 | |||
| 6eced283ec | |||
| 6890b13387 | |||
| d8c24ada78 | |||
| 18f8e2a86c | |||
| abaf0cbdab | |||
| 3560839ef8 | |||
| 4a4024f3f4 | |||
| fee0a27b3f | |||
| ac20b270eb | |||
| 90c733120d | |||
| b6b1f06948 | |||
| 32afc40147 | |||
| 93d2c6ce77 | |||
| 099f7b5aa2 | |||
| 6e9d466172 | |||
| 511e9ab2ad | |||
| 9373dfe847 | |||
| e163bda08f | |||
| 5257d5c6e6 | |||
| 6184102022 | |||
| ba1e17c991 | |||
| fdd38951b5 | |||
| fca4e7fc9c | |||
| fa21066161 | |||
| 4b1bedd2e8 | |||
| be7fc22928 | |||
| 0a229df206 | |||
| c67fb4703d | |||
| d213b1639d | |||
| fc8a060d71 | |||
| e8466143a6 | |||
| 009cc42afd | |||
| cfe0835eea | |||
| a86711c111 | |||
| 9332bc63c8 | |||
| 8217cd5299 | |||
| 44a0f7b132 | |||
| d120fff065 | |||
| 6c38298545 | |||
| a8f9bf3c36 | |||
| c099373d00 | |||
| 4d52302389 | |||
| 093a31a9b5 | |||
| f560e7d2d0 | |||
| 32f1a89bc9 | |||
| 24601722f7 | |||
| fe0d23bd92 | |||
| a23efb69a1 | |||
| 21be815e3b | |||
| 29dab94a9a | |||
| 46cb511e43 | |||
| 61d53e7ea0 | |||
| 25a917866c | |||
| c73e9b9e80 | |||
| d7ad0d7251 | |||
| 3f5859eb3e | |||
| 46e3686550 | |||
| fb545b4122 | |||
| e20ddb4a9c | |||
| 393ea148f0 | |||
| 9029fa1f9d | |||
| e640b6add6 | |||
| b2c56651f9 | |||
| 5c6062420f | |||
| d1ce3924d5 | |||
| d7c2c90290 | |||
| 5eb03c74c4 | |||
| 64bb8a0be8 | |||
| a7c8f8cd17 | |||
| da00407a7c | |||
| cfb9398ec6 | |||
| 966dcc45f2 | |||
| 25f10e6782 | |||
| 3d52d816a4 | |||
| 665c22fc87 | |||
| b897627d72 | |||
| 72cec26c14 | |||
| 7a163389c6 | |||
| 710e0cda8c | |||
| 5c494db262 | |||
| 0969505a71 | |||
| 36694f648d | |||
| f20764adbd | |||
| 712080ff8f | |||
| 76513b8526 | |||
| 0872c4c093 | |||
| c94cb3759a | |||
| 40cbd3fef9 | |||
| cdb12efb62 | |||
| 7bad49522b | |||
| 463801beed | |||
| f02df5a8d0 | |||
| e14f0b6a1d | |||
| bed7ba0a13 | |||
| 2de5f81997 | |||
| 3c6fbba4ea | |||
| a67180a153 | |||
| 3ed84f5ffc | |||
| 1af0f9c06f | |||
| daae6170c7 | |||
| 46bae0a5de | |||
| a6c315ba32 | |||
| ccfaf3592e | |||
| de245ee810 | |||
| e7dd0ce641 | |||
| e1113391cf | |||
| 25b6d6c42b | |||
| 05a8ac0a51 | |||
| 8344808676 | |||
| a192e2c398 | |||
| d99413f896 | |||
| 77ae6bb47d | |||
| 6016c0597b | |||
| eeecab96da | |||
| 127cf36ca9 | |||
| f158afe485 | |||
| 521575ab6a | |||
| c2eefbf244 | |||
| 3f43a77167 | |||
| 9ea9467c2c | |||
| 9a3ee9afa4 | |||
| fd4bb48870 | |||
| d41ce5712d | |||
| ed9f483e7a | |||
| d974d7698b | |||
| bc5d7deda5 | |||
| 44628dfde4 | |||
| 4fd79ffda9 | |||
| fca07fc3c7 | |||
| dfd1ebd2a9 | |||
| 7d6592d1b6 | |||
| e5799ba702 | |||
| 1e1a70ded2 | |||
| 61bd6f2000 | |||
| f651f05a06 | |||
| f3d0192e6b | |||
| 7e429c481b | |||
| 7819d9c5bb | |||
| c0df025e1f | |||
| 5dba22d457 | |||
| 698b1d6333 | |||
| 8b3263bf70 | |||
| 9578dc1b86 | |||
| 45cfa29940 | |||
| d0a8dc0080 | |||
| 8d3cabab9b | |||
| 5ba95a2e6d | |||
| 6d5d614a2c | |||
| 0bac215035 | |||
| 4fddfe2343 | |||
| 9f38148dc4 | |||
| 559493a4b2 | |||
| ed61bb46b6 | |||
| 113a7b8a1b | |||
| 390c5e9a5c | |||
| 0515994833 | |||
| b7717e0e1f | |||
| 1ac70c0405 | |||
| 8ddcd30bc9 | |||
| 82f44284fa | |||
| 6029342c7e | |||
| 2a2d9a786a | |||
| d6ab6e3cc6 | |||
| 5d1d79d9ea | |||
| 34e1ea8a37 | |||
| ef80504a80 | |||
| 51f89b3fe4 | |||
| 50b95de57b | |||
| 345b226f84 | |||
| 8af8bf7512 | |||
| 5ef499a75f | |||
| 79afee2b82 | |||
| 0a977dcd83 | |||
| 74539a6c46 | |||
| dc93704990 | |||
| 02a4a2aa14 |
+104
-160
@@ -5,8 +5,12 @@ name: Release
|
||||
# Порядок (через jobs.needs):
|
||||
# 1) release — контракты + контейнеры + webhook деплоя (атомарно)
|
||||
# 2) publish-packages — npm publish через lerna (если не -alpha)
|
||||
# 3) publish-docs — mkdocs + standards-site → gh-pages (если не -alpha + ветка main)
|
||||
# 4) trigger-coopenomics — repository_dispatch в coopenomics/coopenomics (тот же гейт, что docs)
|
||||
# 3) trigger-coopenomics — workflow_dispatch сборки сайта C9S/coopenomics на
|
||||
# Gitea (если не -alpha + ветка main); он сам тянет
|
||||
# mono и пересобирает доки.
|
||||
#
|
||||
# publish-docs (gh-pages на github.com) удалён 2026-05-25 — после переезда на
|
||||
# Gitea Pages-публикация невалидна; доки деплоятся через C9S/coopenomics.
|
||||
#
|
||||
# Зачем последовательно: пакеты/доки/внешний триггер не должны уезжать,
|
||||
# если релиз контрактов или контейнеров провалился. Раньше четыре workflow'а
|
||||
@@ -34,16 +38,7 @@ permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
# Гейт TS-типов перед сборкой образов. Reusable workflow `typecheck.yaml`
|
||||
# запускает vue-tsc на desktop и tsc --noEmit на controller. Нужно потому,
|
||||
# что `lerna run build` в корневом Dockerfile НЕ ловит TS-ошибки:
|
||||
# quasar build идёт через esbuild с выключенным vueTsc, а у controller'а
|
||||
# build-скрипта вообще нет (ts-node в рантайме).
|
||||
typecheck:
|
||||
uses: ./.github/workflows/typecheck.yaml
|
||||
|
||||
release:
|
||||
needs: typecheck
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
tag_name: ${{ steps.resolve.outputs.tag_name }}
|
||||
@@ -127,14 +122,41 @@ jobs:
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
# === Этап 1: контракты ===
|
||||
# CDT ставим из .deb (C9S/cdt v4.2.0) прямо в окружение job'а и
|
||||
# компилируем напрямую — БЕЗ вложенного docker. Почему не build-all.sh:
|
||||
# тот монтирует $(pwd):/project в sibling-контейнер, а под Gitea
|
||||
# act_runner сам job исполняется в контейнере → хостовый демон не видит
|
||||
# этот путь, /project пуст, cmake падает "no CMakeLists.txt".
|
||||
# build-all.sh остаётся для локальной сборки (оборачивает тот же
|
||||
# build_contracts_cdt.sh в docker).
|
||||
# CMakeLists хардкодит toolchain /cdt/build/...; .deb кладёт CDT в
|
||||
# /usr/opt/cdt/4.2.0, а сам CDTWasmToolchain.cmake указывает на /usr
|
||||
# абсолютно — поэтому симлинк /cdt/build → /usr/opt/cdt/4.2.0 сводит пути
|
||||
# без правки CMakeLists.
|
||||
|
||||
- name: Pull CDT toolchain image
|
||||
run: docker pull dicoop/blockchain_v5.1.1:dev
|
||||
- name: Install CDT 4.2.0 toolchain (.deb)
|
||||
run: |
|
||||
SUDO=""; [ "$(id -u)" -ne 0 ] && SUDO="sudo"
|
||||
$SUDO apt-get update
|
||||
# build-essential — для host-компилятора (project() в CMakeLists);
|
||||
# libz3-4/libtinfo6/libxml2/zlib1g — рантайм бинарей CDT (clang-9,
|
||||
# ld.lld и т.д.). Пакет cdt объявляет только libcurl4-gnutls-dev,
|
||||
# поэтому остальные .so ставим явно — в образе dicoop/blockchain они
|
||||
# были из сборки исходников, из .deb не тянутся.
|
||||
$SUDO apt-get install -y --no-install-recommends \
|
||||
curl ca-certificates cmake make build-essential \
|
||||
libz3-4 libtinfo6 libxml2 zlib1g
|
||||
curl -fsSL -o /tmp/cdt.deb \
|
||||
https://git.coopenomics.world/C9S/cdt/releases/download/v4.2.0/cdt_4.2.0-1_amd64.deb
|
||||
$SUDO apt-get install -y /tmp/cdt.deb
|
||||
$SUDO mkdir -p /cdt
|
||||
$SUDO ln -sfn /usr/opt/cdt/4.2.0 /cdt/build
|
||||
cdt-cpp --version || true
|
||||
|
||||
- name: Compile contracts
|
||||
working-directory: components/contracts
|
||||
run: |
|
||||
./build-all.sh "$BUILD_MODE"
|
||||
./build_contracts_cdt.sh "$BUILD_MODE"
|
||||
echo "--- build/contracts ---"
|
||||
ls -la build/contracts/
|
||||
|
||||
@@ -215,6 +237,17 @@ jobs:
|
||||
- name: Build and push contracts image
|
||||
working-directory: components/contracts
|
||||
run: |
|
||||
# Ретрай push'а: резолвинг registry-1.docker.io на runner'е изредка
|
||||
# моргает (DNS-таймаут к 127.0.0.53) — 3 попытки с паузой.
|
||||
dpush() {
|
||||
local ref="$1" n=1
|
||||
until docker push "$ref"; do
|
||||
[ "$n" -ge 3 ] && { echo "::error::docker push $ref не удался после $n попыток"; return 1; }
|
||||
echo "::warning::docker push $ref упал (попытка $n/3) — повтор через 10с"
|
||||
n=$((n+1)); sleep 10
|
||||
done
|
||||
}
|
||||
|
||||
IMAGE="dicoop/contracts"
|
||||
SHORT_SHA="${SHA::7}"
|
||||
docker build \
|
||||
@@ -223,14 +256,14 @@ jobs:
|
||||
--label "build.mode=$BUILD_MODE" \
|
||||
-t "$IMAGE:$CONTRACTS_TAG" \
|
||||
./docker/.context
|
||||
docker push "$IMAGE:$CONTRACTS_TAG"
|
||||
dpush "$IMAGE:$CONTRACTS_TAG"
|
||||
|
||||
docker tag "$IMAGE:$CONTRACTS_TAG" "$IMAGE:$CONTRACTS_TAG-$SHORT_SHA"
|
||||
docker push "$IMAGE:$CONTRACTS_TAG-$SHORT_SHA"
|
||||
dpush "$IMAGE:$CONTRACTS_TAG-$SHORT_SHA"
|
||||
|
||||
if [ -n "$CONTRACTS_EXTRA" ]; then
|
||||
docker tag "$IMAGE:$CONTRACTS_TAG" "$IMAGE:$CONTRACTS_EXTRA"
|
||||
docker push "$IMAGE:$CONTRACTS_EXTRA"
|
||||
dpush "$IMAGE:$CONTRACTS_EXTRA"
|
||||
fi
|
||||
|
||||
- name: Verify pushed contracts image
|
||||
@@ -243,15 +276,31 @@ jobs:
|
||||
|
||||
- name: Build and push base image
|
||||
run: |
|
||||
dpush() {
|
||||
local ref="$1" n=1
|
||||
until docker push "$ref"; do
|
||||
[ "$n" -ge 3 ] && { echo "::error::docker push $ref не удался после $n попыток"; return 1; }
|
||||
echo "::warning::docker push $ref упал (попытка $n/3) — повтор через 10с"
|
||||
n=$((n+1)); sleep 10
|
||||
done
|
||||
}
|
||||
docker build --target runtime -t "dicoop/mono-base:$TAG_NAME" .
|
||||
docker push "dicoop/mono-base:$TAG_NAME"
|
||||
dpush "dicoop/mono-base:$TAG_NAME"
|
||||
if [ "$IS_PROD" = "true" ]; then
|
||||
docker tag "dicoop/mono-base:$TAG_NAME" dicoop/mono-base:latest
|
||||
docker push dicoop/mono-base:latest
|
||||
dpush dicoop/mono-base:latest
|
||||
fi
|
||||
|
||||
- name: Build and push service images
|
||||
run: |
|
||||
dpush() {
|
||||
local ref="$1" n=1
|
||||
until docker push "$ref"; do
|
||||
[ "$n" -ge 3 ] && { echo "::error::docker push $ref не удался после $n попыток"; return 1; }
|
||||
echo "::warning::docker push $ref упал (попытка $n/3) — повтор через 10с"
|
||||
n=$((n+1)); sleep 10
|
||||
done
|
||||
}
|
||||
build_service() {
|
||||
local SVC="$1" PKG="$2" CMD="$3"
|
||||
local DOCKERFILE="Dockerfile.$SVC"
|
||||
@@ -260,10 +309,10 @@ jobs:
|
||||
echo "CMD [\"pnpm\", \"-F\", \"$PKG\", \"run\", \"$CMD\"]"
|
||||
} > "$DOCKERFILE"
|
||||
docker build -t "dicoop/$SVC:$TAG_NAME" -f "$DOCKERFILE" .
|
||||
docker push "dicoop/$SVC:$TAG_NAME"
|
||||
dpush "dicoop/$SVC:$TAG_NAME"
|
||||
if [ "$IS_PROD" = "true" ]; then
|
||||
docker tag "dicoop/$SVC:$TAG_NAME" "dicoop/$SVC:latest"
|
||||
docker push "dicoop/$SVC:latest"
|
||||
dpush "dicoop/$SVC:latest"
|
||||
fi
|
||||
}
|
||||
|
||||
@@ -314,149 +363,44 @@ jobs:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
# ============================================================================
|
||||
# publish-docs — mkdocs + standards-site → gh-pages (бывший publish-docs.yaml).
|
||||
# trigger-coopenomics-docs — пересборка сайта C9S/coopenomics.
|
||||
# Гейт: не-alpha + ветка main.
|
||||
# ВНИМАНИЕ: gh-pages step пушит на github.com/coopenomics/mono — это GitHub
|
||||
# Pages. При переезде на Gitea Actions нужно будет либо подложить PAT для
|
||||
# внешнего push'а в GitHub, либо переключить публикацию на Gitea Pages.
|
||||
# ============================================================================
|
||||
publish-docs:
|
||||
needs: release
|
||||
if: ${{ !contains(github.ref, '-alpha') && needs.release.outputs.branch == 'main' }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
# Версия pnpm берётся из `packageManager` корневого package.json,
|
||||
# синхронно с publish-packages job'ом и Dockerfile'ами.
|
||||
- name: Set up pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
cache: pnpm
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: '3.10'
|
||||
|
||||
- name: Install Python requirements
|
||||
run: |
|
||||
python -m venv venv
|
||||
source venv/bin/activate
|
||||
pip install mkdocs-material mkdocs-macros-plugin mkdocs-section-index pymdown-extensions
|
||||
working-directory: ./components/docs
|
||||
|
||||
- name: Install Node.js dependencies
|
||||
run: pnpm install
|
||||
working-directory: ./components/docs
|
||||
|
||||
- name: Patch spectaql-config.yml for CI
|
||||
run: |
|
||||
sed -i.bak "0,/url:.*/s|url:.*|url: 'https://testnet.coopenomics.world/backend/v1/graphql'|" spectaql-config.yml
|
||||
working-directory: ./components/controller
|
||||
|
||||
- name: Show patched spectaql-config.yml
|
||||
run: cat spectaql-config.yml
|
||||
working-directory: ./components/controller
|
||||
|
||||
- name: Build cooptypes
|
||||
run: pnpm run build
|
||||
working-directory: ./components/cooptypes
|
||||
|
||||
- name: Generate controller docs
|
||||
run: pnpm run docs
|
||||
working-directory: ./components/controller
|
||||
|
||||
- name: Copy controller docs
|
||||
run: |
|
||||
mkdir -p ./components/docs/docs/graphql
|
||||
cp -r ./components/controller/docs/* ./components/docs/docs/graphql/
|
||||
|
||||
- name: Generate sdk docs
|
||||
run: pnpm run docs
|
||||
working-directory: ./components/sdk
|
||||
|
||||
- name: Copy sdk docs
|
||||
run: |
|
||||
mkdir -p ./components/docs/docs/sdk
|
||||
cp -r ./components/sdk/docs/* ./components/docs/docs/sdk/
|
||||
|
||||
- name: Generate cooptypes docs
|
||||
run: pnpm run docs
|
||||
working-directory: ./components/cooptypes
|
||||
|
||||
- name: Copy cooptypes docs
|
||||
run: |
|
||||
mkdir -p ./components/docs/docs/cooptypes
|
||||
cp -r ./components/cooptypes/docs/* ./components/docs/docs/cooptypes/
|
||||
|
||||
# ── standards-site → /standards/ на docs.цифровой-кооператив.рф ──
|
||||
# Отдельный Vue-сайт с BPMN-графом кооперативных стандартов,
|
||||
# публикуется как поддиректория на том же домене. Vite собирает
|
||||
# с base='/standards/' (см. vite.config.ts), относительные пути
|
||||
# внутри dist/ и hash-router работают корректно.
|
||||
# dist кладём в docs/standards/ ДО mkdocs build — так же, как
|
||||
# graphql/sdk/cooptypes; mkdocs сам включит его в итоговый site/.
|
||||
- name: Build standards-site
|
||||
run: pnpm run build
|
||||
working-directory: ./components/contracts/standards-site
|
||||
|
||||
- name: Copy standards-site into docs/standards
|
||||
run: |
|
||||
mkdir -p ./components/docs/docs/standards
|
||||
cp -r ./components/contracts/standards-site/dist/* ./components/docs/docs/standards/
|
||||
|
||||
- name: Build docs (mkdocs)
|
||||
run: |
|
||||
source venv/bin/activate
|
||||
mkdocs build
|
||||
working-directory: ./components/docs
|
||||
|
||||
- name: Remove specific large file before publishing
|
||||
run: |
|
||||
rm -f ./components/docs/site/sdk/typedoc.json
|
||||
if [ -f "./components/docs/site/sdk/typedoc.json" ]; then
|
||||
echo "ERROR: typedoc.json still exists!"
|
||||
exit 1
|
||||
else
|
||||
echo "SUCCESS: typedoc.json removed successfully"
|
||||
fi
|
||||
|
||||
- name: Publish to GitHub Pages
|
||||
run: npx gh-pages --nojekyll -d site --repo https://x-access-token:${GITHUB_TOKEN}@github.com/coopenomics/mono.git
|
||||
working-directory: ./components/docs
|
||||
env:
|
||||
GIT_AUTHOR_NAME: github-actions
|
||||
GIT_AUTHOR_EMAIL: github-actions@github.com
|
||||
GIT_COMMITTER_NAME: github-actions
|
||||
GIT_COMMITTER_EMAIL: github-actions@github.com
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Trigger docs deployment webhook
|
||||
if: ${{ success() }}
|
||||
run: |
|
||||
curl -X POST "${{ vars.DOCS_DEPLOY_WEBHOOK_URL }}" \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"ref":"${{ github.ref }}","sha":"${{ github.sha }}","branch":"${{ github.ref_name }}"}'
|
||||
|
||||
# ============================================================================
|
||||
# trigger-coopenomics-docs — repository_dispatch в coopenomics/coopenomics
|
||||
# (бывший build-contracts-docs.yaml). Гейт идентичен publish-docs.
|
||||
#
|
||||
# coopenomics переехал на Gitea (C9S/coopenomics); его publish-docs.yaml
|
||||
# клонирует mono и пересобирает сайт. Gitea НЕ имеет API для
|
||||
# repository_dispatch — только workflow_dispatch, поэтому дёргаем целевой
|
||||
# workflow через Gitea API с PAT (secret DOCS_DISPATCH_TOKEN — префикс GITEA_
|
||||
# у секретов зарезервирован Gitea, нельзя; права write на C9S/coopenomics).
|
||||
# Целевой workflow слушает workflow_dispatch с входами
|
||||
# mono_sha/mono_ref.
|
||||
# ============================================================================
|
||||
trigger-coopenomics-docs:
|
||||
needs: release
|
||||
if: ${{ !contains(github.ref, '-alpha') && needs.release.outputs.branch == 'main' }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Trigger Coopenomics deployment
|
||||
uses: peter-evans/repository-dispatch@v2
|
||||
with:
|
||||
token: ${{ secrets.COOPENOMICS_PAT }}
|
||||
repository: coopenomics/coopenomics
|
||||
event-type: deploy_from_mono
|
||||
client-payload: '{"repository": "${{ github.repository }}", "sha": "${{ github.sha }}", "ref": "${{ github.ref }}", "actor": "${{ github.actor }}"}'
|
||||
- name: Trigger coopenomics website rebuild (Gitea workflow_dispatch)
|
||||
run: |
|
||||
curl -fsSL -X POST \
|
||||
-H "Authorization: token ${{ secrets.DOCS_DISPATCH_TOKEN }}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"ref":"master","inputs":{"mono_sha":"${{ github.sha }}","mono_ref":"${{ github.ref }}"}}' \
|
||||
"${{ github.server_url }}/api/v1/repos/C9S/coopenomics/actions/workflows/publish-docs.yaml/dispatches"
|
||||
|
||||
# ============================================================================
|
||||
# trigger-mono-docs — деплой ВТОРОЙ документации (сайт доков mono) через
|
||||
# webhook DOCS_DEPLOY_WEBHOOK_URL. Отдельная от coopenomics публикация —
|
||||
# обе доки уезжают синхронно по релизу. Извлечён из бывшего publish-docs:
|
||||
# gh-pages на github.com выпилен как мёртвый, остался реальный deploy-webhook
|
||||
# (приёмник деплоит доки на своей стороне). Гейт: не-alpha + ветка main.
|
||||
# ============================================================================
|
||||
trigger-mono-docs:
|
||||
needs: release
|
||||
if: ${{ !contains(github.ref, '-alpha') && needs.release.outputs.branch == 'main' }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Trigger docs deployment webhook
|
||||
run: |
|
||||
curl -fsSL -X POST "${{ vars.DOCS_DEPLOY_WEBHOOK_URL }}" \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"ref":"${{ github.ref }}","sha":"${{ github.sha }}","branch":"${{ github.ref_name }}"}'
|
||||
|
||||
@@ -13,11 +13,11 @@ name: Typecheck
|
||||
#
|
||||
# Триггеры:
|
||||
# - pull_request на dev — гейт перед мерджем в основную ветку;
|
||||
# - workflow_call — переиспользуется из release.yaml как `needs:` у release-job,
|
||||
# чтобы битый тэг не уехал в DockerHub.
|
||||
# - workflow_call — оставлен для переиспользования, но release.yaml его
|
||||
# больше НЕ вызывает (гейт из релиза убран как избыточный — типы
|
||||
# проверяются на PR в dev до того, как код доедет до тэга).
|
||||
#
|
||||
# Push в dev/testnet/main НЕ триггерит — намеренно (PR-гейт достаточен,
|
||||
# а тэги покрывает workflow_call из release.yaml).
|
||||
# Push в dev/testnet/main НЕ триггерит — намеренно (PR-гейт на dev достаточен).
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
node_modules/
|
||||
node_modules
|
||||
lerna-debug.log
|
||||
components/controller/graph.png
|
||||
blockchain-data/
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@coopenomics/blago-cli",
|
||||
"version": "2026.5.23-4",
|
||||
"version": "2026.5.25-3",
|
||||
"description": "CLI синхронизации артефактов Благорост с бэкендом через @coopenomics/sdk",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
|
||||
@@ -9,12 +9,15 @@ export interface CommunicationCursorsFile {
|
||||
messageLastTsByRoom: Record<string, number>
|
||||
/** project_hash → ISO instant: транскрипции с endedAt ≤ этого момента уже выгружены */
|
||||
transcriptionLastEndedExclusiveByProject: Record<string, string>
|
||||
/** matrixRoomId → ISO instant: транскрипции непроектной комнаты с endedAt ≤ этого момента уже выгружены */
|
||||
transcriptionLastEndedExclusiveByRoom: Record<string, string>
|
||||
}
|
||||
|
||||
function empty(): CommunicationCursorsFile {
|
||||
return {
|
||||
messageLastTsByRoom: {},
|
||||
transcriptionLastEndedExclusiveByProject: {},
|
||||
transcriptionLastEndedExclusiveByRoom: {},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,6 +35,11 @@ export async function loadCommunicationCursors(root: string): Promise<Communicat
|
||||
&& typeof parsed.transcriptionLastEndedExclusiveByProject === 'object'
|
||||
? { ...parsed.transcriptionLastEndedExclusiveByProject }
|
||||
: {},
|
||||
transcriptionLastEndedExclusiveByRoom:
|
||||
parsed.transcriptionLastEndedExclusiveByRoom !== undefined
|
||||
&& typeof parsed.transcriptionLastEndedExclusiveByRoom === 'object'
|
||||
? { ...parsed.transcriptionLastEndedExclusiveByRoom }
|
||||
: {},
|
||||
}
|
||||
}
|
||||
catch {
|
||||
|
||||
@@ -25,7 +25,7 @@ import {
|
||||
transcriptionMeetingFileStemUtc,
|
||||
type CommunicationDayLine,
|
||||
} from './communication-markdown.js'
|
||||
import { workspaceBasePath, type ProjectPathModel } from './layout.js'
|
||||
import { generateSlug, workspaceBasePath, type ProjectPathModel } from './layout.js'
|
||||
import { syncEntityFile } from './sync-entity-file.js'
|
||||
|
||||
interface ProjectRowLite {
|
||||
@@ -183,6 +183,19 @@ async function listRooms(ctx: AuthenticatedContext, projectHash: string) {
|
||||
return q[Queries.ChatCoop.ListProjectCommunicationRooms.name] ?? []
|
||||
}
|
||||
|
||||
/** Стабильная папка непроектной комнаты в `rooms/`. Системные — фиксированные, комнаты секретаря — slug + хвост id (уникальность). */
|
||||
function nonProjectRoomFolder(kind: string, matrixRoomId: string, displayLabel: string): string {
|
||||
if (kind === 'MEMBERS') {
|
||||
return 'komnata-paishchikov'
|
||||
}
|
||||
if (kind === 'COUNCIL') {
|
||||
return 'komnata-soveta'
|
||||
}
|
||||
const slug = generateSlug(displayLabel) || 'komnata'
|
||||
const shortId = createHash('sha256').update(matrixRoomId, 'utf8').digest('hex').slice(0, 6)
|
||||
return `${slug}-${shortId}`
|
||||
}
|
||||
|
||||
export async function pullProjectCommunicationArtifacts(
|
||||
ctx: AuthenticatedContext,
|
||||
index: IndexFile,
|
||||
@@ -422,3 +435,208 @@ export async function pullProjectCommunicationArtifacts(
|
||||
warn(`Не удалось сохранить курсоры переписки: ${formatThrownValue(e)}`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Pull переписки и транскрипций из комнат ВНЕ проектов Capital (пайщики, совет, комнаты секретаря).
|
||||
* Раскладка — отдельная верхняя папка `rooms/<folder>/{messages,meetings}/`, чтобы не смешивать с
|
||||
* проектными `meetings/`. Логика идентична проектной, но bucket = одна комната, курсор транскрипций — по matrixRoomId.
|
||||
*/
|
||||
export async function pullNonProjectCommunicationArtifacts(
|
||||
ctx: AuthenticatedContext,
|
||||
index: IndexFile,
|
||||
): Promise<void> {
|
||||
let rooms: { matrixRoomId: string, displayLabel: string, kind: string }[]
|
||||
try {
|
||||
const q = await ctx.client.Query(Queries.ChatCoop.ListNonProjectCommunicationRooms.query, {})
|
||||
rooms = (q[Queries.ChatCoop.ListNonProjectCommunicationRooms.name] ?? []) as typeof rooms
|
||||
}
|
||||
catch (e) {
|
||||
warn(`Список непроектных комнат (chatcoopListNonProjectCommunicationRooms): ${formatThrownValue(e)}`)
|
||||
return
|
||||
}
|
||||
if (rooms.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
let cursors: CommunicationCursorsFile
|
||||
try {
|
||||
cursors = await loadCommunicationCursors(ctx.root)
|
||||
}
|
||||
catch (e) {
|
||||
warn(`Курсоры переписки (комнаты): не удалось прочитать, начинаем с пустых: ${formatThrownValue(e)}`)
|
||||
cursors = {
|
||||
messageLastTsByRoom: {},
|
||||
transcriptionLastEndedExclusiveByProject: {},
|
||||
transcriptionLastEndedExclusiveByRoom: {},
|
||||
}
|
||||
}
|
||||
|
||||
for (const room of rooms) {
|
||||
const folder = nonProjectRoomFolder(room.kind, room.matrixRoomId, room.displayLabel)
|
||||
const basePath = `rooms/${folder}`
|
||||
const roomTitle = room.displayLabel || room.matrixRoomId
|
||||
|
||||
// Сообщения комнаты — по календарным суткам UTC новее курсора.
|
||||
try {
|
||||
const last = cursors.messageLastTsByRoom[room.matrixRoomId]
|
||||
const afterTs = last ?? 0
|
||||
const datesQ = await ctx.client.Query(Queries.ChatCoop.ListUtcDatesWithNewRoomMessages.query, {
|
||||
variables: { data: { matrixRoomId: room.matrixRoomId, afterOriginServerTsExclusive: afterTs } },
|
||||
})
|
||||
const dates = (datesQ[Queries.ChatCoop.ListUtcDatesWithNewRoomMessages.name] ?? []).sort()
|
||||
for (const utcDate of dates) {
|
||||
const mq = await ctx.client.Query(Queries.ChatCoop.GetRoomMessagesForUtcDate.query, {
|
||||
variables: { data: { matrixRoomId: room.matrixRoomId, utcDate } },
|
||||
})
|
||||
const linesRaw = mq[Queries.ChatCoop.GetRoomMessagesForUtcDate.name] ?? []
|
||||
const lines: CommunicationDayLine[] = linesRaw.map(m => ({
|
||||
originServerTs: m.originServerTs,
|
||||
authorLabel: m.authorLabel,
|
||||
coopUsername: m.coopUsername,
|
||||
kind: String(m.kind),
|
||||
bodyText: m.bodyText,
|
||||
}))
|
||||
if (lines.length === 0) {
|
||||
continue
|
||||
}
|
||||
const content = projectCommunicationDayToMarkdown(roomTitle, room.matrixRoomId, utcDate, [
|
||||
{ displayLabel: room.displayLabel, matrixRoomId: room.matrixRoomId, lines },
|
||||
])
|
||||
const rel = `${basePath}/messages/${utcDate}.md`
|
||||
const entityHash = messageDayEntityHash(room.matrixRoomId, utcDate)
|
||||
await syncEntityFile({
|
||||
root: ctx.root,
|
||||
index,
|
||||
entityType: 'room_message_day',
|
||||
entityHash,
|
||||
relativePath: rel,
|
||||
content,
|
||||
remoteUpdatedAt: `${utcDate}T23:59:59.999Z`,
|
||||
label: `переписка ${utcDate} (${room.matrixRoomId})`,
|
||||
})
|
||||
}
|
||||
|
||||
const maxQ = await ctx.client.Query(Queries.ChatCoop.GetMaxOriginServerTsForRoom.query, {
|
||||
variables: { data: { matrixRoomId: room.matrixRoomId } },
|
||||
})
|
||||
const maxTs = maxQ[Queries.ChatCoop.GetMaxOriginServerTsForRoom.name] as number | null | undefined
|
||||
if (maxTs !== undefined && maxTs !== null && Number.isFinite(maxTs)) {
|
||||
cursors.messageLastTsByRoom[room.matrixRoomId] = maxTs
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
warn(`Переписка Matrix, комната ${room.matrixRoomId} (${roomTitle}): ${formatThrownValue(e)}`)
|
||||
}
|
||||
|
||||
// Транскрипции звонков комнаты + sibling memo.
|
||||
try {
|
||||
const tExIso = cursors.transcriptionLastEndedExclusiveByRoom[room.matrixRoomId]
|
||||
const lowerBoundExclusive = tExIso === undefined ? new Date(0) : new Date(tExIso)
|
||||
|
||||
interface TranscriptionCandidate {
|
||||
id: string
|
||||
endedAt: Date
|
||||
memo: string
|
||||
updatedAt: Date | undefined
|
||||
}
|
||||
const byId = new Map<string, TranscriptionCandidate>()
|
||||
const tq = await ctx.client.Query(Queries.ChatCoop.GetTranscriptions.query, {
|
||||
variables: { data: { matrixRoomId: room.matrixRoomId, limit: CHATCOOP_TRANSCRIPTIONS_QUERY_LIMIT, offset: 0 } },
|
||||
})
|
||||
const list = tq[Queries.ChatCoop.GetTranscriptions.name] ?? []
|
||||
for (const t of list) {
|
||||
const end = dateFromUnknown(t.endedAt)
|
||||
if (t.status !== Zeus.TranscriptionStatus.COMPLETED || !end) {
|
||||
continue
|
||||
}
|
||||
const prev = byId.get(t.id)
|
||||
if (!prev || end > prev.endedAt) {
|
||||
byId.set(t.id, {
|
||||
id: t.id,
|
||||
endedAt: end,
|
||||
memo: typeof t.memo === 'string' ? t.memo : '',
|
||||
updatedAt: dateFromUnknown(t.updatedAt),
|
||||
})
|
||||
}
|
||||
}
|
||||
const allCompleted = [...byId.values()].sort((a, b) => a.endedAt.getTime() - b.endedAt.getTime())
|
||||
|
||||
const newMeetings = allCompleted.filter(c => c.endedAt.getTime() > lowerBoundExclusive.getTime())
|
||||
let maxEnded: Date | null = null
|
||||
for (const c of newMeetings) {
|
||||
const packQ = await ctx.client.Query(Queries.ChatCoop.GetTranscription.query, {
|
||||
variables: { data: { id: c.id } },
|
||||
})
|
||||
const pack = packQ[Queries.ChatCoop.GetTranscription.name]
|
||||
if (!pack?.transcription || pack.transcription.status !== Zeus.TranscriptionStatus.COMPLETED) {
|
||||
continue
|
||||
}
|
||||
const tr = pack.transcription
|
||||
const startedAt: Date | string = dateFromUnknown(tr.startedAt) ?? (tr.startedAt as Date | string)
|
||||
const endedAtTr: Date | string | null | undefined
|
||||
= dateFromUnknown(tr.endedAt) ?? (tr.endedAt as Date | string | null | undefined)
|
||||
const md = renderCallTranscriptionMarkdown(
|
||||
{
|
||||
matrixRoomId: String(tr.matrixRoomId),
|
||||
roomId: String(tr.roomId),
|
||||
startedAt,
|
||||
endedAt: endedAtTr,
|
||||
},
|
||||
pack.segments.map(s => ({
|
||||
speakerName: s.speakerName,
|
||||
text: s.text,
|
||||
startOffset: s.startOffset,
|
||||
endOffset: s.endOffset,
|
||||
})),
|
||||
)
|
||||
const stem = transcriptionMeetingFileStemUtc(c.endedAt)
|
||||
const rel = `${basePath}/meetings/${stem}.md`
|
||||
const entityHash = c.id.toLowerCase()
|
||||
await syncEntityFile({
|
||||
root: ctx.root,
|
||||
index,
|
||||
entityType: 'call_transcription',
|
||||
entityHash,
|
||||
relativePath: rel,
|
||||
content: md,
|
||||
remoteUpdatedAt: toUpdatedIso(c.endedAt),
|
||||
label: `транскрипция ${c.id}`,
|
||||
})
|
||||
if (!maxEnded || c.endedAt > maxEnded) {
|
||||
maxEnded = c.endedAt
|
||||
}
|
||||
}
|
||||
|
||||
for (const c of allCompleted) {
|
||||
const stem = transcriptionMeetingFileStemUtc(c.endedAt)
|
||||
const memoRel = `${basePath}/meetings/${stem}.memo.md`
|
||||
const memoRemoteUpdatedAt = toUpdatedIso(c.updatedAt ?? c.endedAt)
|
||||
await syncTranscriptionMemoFile({
|
||||
root: ctx.root,
|
||||
index,
|
||||
transcriptionId: c.id,
|
||||
relativePath: memoRel,
|
||||
serverMemo: c.memo,
|
||||
remoteUpdatedAtIso: memoRemoteUpdatedAt,
|
||||
})
|
||||
}
|
||||
|
||||
if (maxEnded) {
|
||||
cursors.transcriptionLastEndedExclusiveByRoom[room.matrixRoomId] = maxEnded.toISOString()
|
||||
}
|
||||
else if (tExIso === undefined) {
|
||||
cursors.transcriptionLastEndedExclusiveByRoom[room.matrixRoomId] = new Date().toISOString()
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
warn(`Транскрипции звонков, комната ${room.matrixRoomId}: ${formatThrownValue(e)}`)
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await saveCommunicationCursors(ctx.root, cursors)
|
||||
}
|
||||
catch (e) {
|
||||
warn(`Не удалось сохранить курсоры переписки (комнаты): ${formatThrownValue(e)}`)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ import {
|
||||
storyFileRelativePath,
|
||||
workspaceBasePath,
|
||||
} from './layout.js'
|
||||
import { pullProjectCommunicationArtifacts } from './pull-communication.js'
|
||||
import { pullNonProjectCommunicationArtifacts, pullProjectCommunicationArtifacts } from './pull-communication.js'
|
||||
import { scaffoldBmadWorkspacesAfterPull } from './scaffold-bmad-workspace.js'
|
||||
import { syncEntityFile } from './sync-entity-file.js'
|
||||
import { writeWorkspaceIndexMarkdown } from './workspace-index.js'
|
||||
@@ -329,6 +329,8 @@ export async function runPull(ctx: AuthenticatedContext, options: RunPullOptions
|
||||
|
||||
await pullProjectCommunicationArtifacts(ctx, index, allProjects, projectByHash)
|
||||
|
||||
await pullNonProjectCommunicationArtifacts(ctx, index)
|
||||
|
||||
await scaffoldBmadWorkspacesAfterPull(ctx, allProjects, projectByHash)
|
||||
|
||||
await saveIndex(ctx.root, index)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@coopenomics/boot",
|
||||
"type": "module",
|
||||
"version": "2026.5.23-4",
|
||||
"version": "2026.5.25-3",
|
||||
"private": true,
|
||||
"packageManager": "pnpm@9.0.6",
|
||||
"description": "CLI-утилита инициализации блокчейна и кооператива",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Загружаем per-instance конфиг из корня репо.
|
||||
# Загружаем per-instance конфиг из корня репо (для CHAIN_URL/MONGODB_URL/API_URL,
|
||||
# которые читают TS-код boot и шелл-скрипты networks.sh/preactivate.sh).
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd)"
|
||||
if [ -f "$ROOT_DIR/.env" ]; then
|
||||
set -a
|
||||
@@ -11,24 +12,29 @@ fi
|
||||
|
||||
# Останавливаем и удаляем контейнеры вместе с volumes
|
||||
echo "Останавливаем и удаляем контейнеры с volumes..."
|
||||
docker compose down -v mongo postgres cooparser || true
|
||||
docker compose down -v mongo postgres monoredis cooparser coopback || true
|
||||
|
||||
# Останавливаем blockchain контейнер перед удалением данных
|
||||
echo "Останавливаем blockchain контейнер..."
|
||||
docker compose stop node || true
|
||||
|
||||
# Удаляем blockchain data
|
||||
# Удаляем blockchain data.
|
||||
# Контейнерный wipe (alpine под root) стирает данные независимо от их владельца —
|
||||
# без sudo на любой ноде (на Pi нет passwordless sudo; на проде nodeos пишет
|
||||
# данные под root). Единый способ с reboot.sh / extra_reboot.sh.
|
||||
echo "Удаляем blockchain data..."
|
||||
# sudo chmod -R 755 ../blockchain-data/ 2>/dev/null || true
|
||||
sudo rm -rf ../blockchain-data/
|
||||
docker run --rm -v "$(cd .. && pwd)/blockchain-data:/d" alpine sh -c 'rm -rf /d/* /d/.[!.]* 2>/dev/null || true'
|
||||
|
||||
# Пересоздаем и запускаем базы данных
|
||||
# Пересоздаем и запускаем базы данных + Redis (monoredis).
|
||||
# monoredis ОБЯЗАТЕЛЕН: без него coopback падает на старте с
|
||||
# `getaddrinfo EAI_AGAIN monoredis` → MaxRetriesPerRequestError → nodemon crash.
|
||||
echo "Пересоздаем и запускаем базы данных..."
|
||||
docker compose up -d mongo postgres
|
||||
docker compose up -d mongo postgres monoredis
|
||||
|
||||
# Ждем готовности MongoDB
|
||||
# Ждем готовности MongoDB (standalone, ping вместо ожидания PRIMARY).
|
||||
echo "Ждем готовности MongoDB..."
|
||||
until docker compose exec -T mongo mongosh --eval "db.adminCommand('ping')" --quiet > /dev/null 2>&1; do
|
||||
until docker compose exec -T mongo mongosh --quiet --eval "db.adminCommand({ping:1}).ok" > /dev/null 2>&1; do
|
||||
echo "MongoDB еще не готов, ждем..."
|
||||
sleep 2
|
||||
done
|
||||
@@ -42,7 +48,7 @@ until docker compose exec -T postgres pg_isready -U postgres -d voskhod > /dev/n
|
||||
done
|
||||
echo "PostgreSQL готов!"
|
||||
|
||||
# Запускаем boot процесс
|
||||
# Запускаем boot процесс (clean: только программы Благорост/маркетплейс)
|
||||
echo "Запускаем boot процесс..."
|
||||
pnpm run boot:clean
|
||||
|
||||
@@ -51,6 +57,6 @@ echo "Запускаем parser..."
|
||||
docker compose up -d cooparser
|
||||
|
||||
echo "Запускаем контроллер..."
|
||||
docker compose restart coopback || true
|
||||
docker compose up -d --force-recreate coopback || true
|
||||
|
||||
echo "Перезапуск завершен!"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Загружаем per-instance конфиг из корня репо.
|
||||
# Загружаем per-instance конфиг из корня репо (для CHAIN_URL/MONGODB_URL/API_URL,
|
||||
# которые читают TS-код boot и шелл-скрипты networks.sh/preactivate.sh).
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd)"
|
||||
if [ -f "$ROOT_DIR/.env" ]; then
|
||||
set -a
|
||||
@@ -9,30 +10,32 @@ if [ -f "$ROOT_DIR/.env" ]; then
|
||||
set +a
|
||||
fi
|
||||
|
||||
# Останавливаем контроллер перед очисткой данных
|
||||
echo "Останавливаем контроллер..."
|
||||
docker compose down coopback || true
|
||||
|
||||
# Останавливаем и удаляем контейнеры вместе с volumes
|
||||
echo "Останавливаем и удаляем контейнеры с volumes..."
|
||||
docker compose down -v mongo postgres cooparser || true
|
||||
docker compose down -v mongo postgres monoredis cooparser coopback || true
|
||||
|
||||
# Останавливаем blockchain контейнер перед удалением данных
|
||||
echo "Останавливаем blockchain контейнер..."
|
||||
docker compose stop node || true
|
||||
|
||||
# Удаляем blockchain data
|
||||
# Удаляем blockchain data.
|
||||
# Контейнерный wipe (alpine под root) стирает данные независимо от их владельца —
|
||||
# без sudo на любой ноде (на Pi нет passwordless sudo; на проде nodeos пишет
|
||||
# данные под root). Единый способ с reboot.sh / clean_reboot.sh.
|
||||
echo "Удаляем blockchain data..."
|
||||
# sudo chmod -R 755 ../blockchain-data/ 2>/dev/null || true
|
||||
sudo rm -rf ../blockchain-data/
|
||||
docker run --rm -v "$(cd .. && pwd)/blockchain-data:/d" alpine sh -c 'rm -rf /d/* /d/.[!.]* 2>/dev/null || true'
|
||||
|
||||
# Пересоздаем и запускаем базы данных
|
||||
# Пересоздаем и запускаем базы данных + Redis (monoredis).
|
||||
# monoredis ОБЯЗАТЕЛЕН: без него coopback падает на старте с
|
||||
# `getaddrinfo EAI_AGAIN monoredis` → MaxRetriesPerRequestError → nodemon crash,
|
||||
# и провайдер не может взять org-данные partner1 (PROVIDER_URL=coopback:2998).
|
||||
echo "Пересоздаем и запускаем базы данных..."
|
||||
docker compose up -d mongo postgres
|
||||
docker compose up -d mongo postgres monoredis
|
||||
|
||||
# Ждем готовности MongoDB
|
||||
# Ждем готовности MongoDB (standalone, ping вместо ожидания PRIMARY).
|
||||
echo "Ждем готовности MongoDB..."
|
||||
until docker compose exec -T mongo mongosh --eval "db.adminCommand('ping')" --quiet > /dev/null 2>&1; do
|
||||
until docker compose exec -T mongo mongosh --quiet --eval "db.adminCommand({ping:1}).ok" > /dev/null 2>&1; do
|
||||
echo "MongoDB еще не готов, ждем..."
|
||||
sleep 2
|
||||
done
|
||||
@@ -46,7 +49,7 @@ until docker compose exec -T postgres pg_isready -U postgres -d voskhod > /dev/n
|
||||
done
|
||||
echo "PostgreSQL готов!"
|
||||
|
||||
# Запускаем boot процесс
|
||||
# Запускаем boot процесс (расширенный: совет + пайщики; partner1 — при EXTRA_RENT=1)
|
||||
echo "Запускаем boot процесс..."
|
||||
pnpm run boot:extra
|
||||
|
||||
@@ -55,6 +58,6 @@ echo "Запускаем parser..."
|
||||
docker compose up -d cooparser
|
||||
|
||||
echo "Запускаем контроллер..."
|
||||
docker compose up -d coopback
|
||||
docker compose up -d --force-recreate coopback || true
|
||||
|
||||
echo "Перезапуск завершен!"
|
||||
|
||||
@@ -291,31 +291,46 @@ export default class Blockchain {
|
||||
async activateFeature(feature: Feature) {
|
||||
await this.update_pass_instance()
|
||||
|
||||
await this.api.transact(
|
||||
{
|
||||
actions: [
|
||||
{
|
||||
account: 'eosio',
|
||||
name: 'activate',
|
||||
authorization: [
|
||||
{
|
||||
actor: 'eosio',
|
||||
permission: 'active',
|
||||
try {
|
||||
await this.api.transact(
|
||||
{
|
||||
actions: [
|
||||
{
|
||||
account: 'eosio',
|
||||
name: 'activate',
|
||||
authorization: [
|
||||
{
|
||||
actor: 'eosio',
|
||||
permission: 'active',
|
||||
},
|
||||
],
|
||||
data: {
|
||||
feature_digest: feature.hash,
|
||||
},
|
||||
],
|
||||
data: {
|
||||
feature_digest: feature.hash,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
blocksBehind: 3,
|
||||
expireSeconds: 30,
|
||||
},
|
||||
)
|
||||
],
|
||||
},
|
||||
{
|
||||
blocksBehind: 3,
|
||||
expireSeconds: 30,
|
||||
},
|
||||
)
|
||||
|
||||
console.log('Фича активирована: ', feature.name)
|
||||
console.log('Фича активирована: ', feature.name)
|
||||
}
|
||||
catch (e: any) {
|
||||
// На уже забутстрапленном (не обнулённом) чейне протокол-фича уже активна —
|
||||
// nodeos отдаёт protocol_feature_exception (code 3250000). Это не повод
|
||||
// ронять весь boot: фича на месте, нужный результат достигнут, продолжаем.
|
||||
const errName = e?.json?.error?.name
|
||||
const errCode = e?.json?.error?.code
|
||||
const msg = e instanceof Error ? e.message : String(e)
|
||||
if (errName === 'protocol_feature_exception' || errCode === 3250000 || /already activated/i.test(msg)) {
|
||||
console.warn(`Фича уже активирована, пропускаю: ${feature.name}`)
|
||||
return
|
||||
}
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
async createToken(params: TokenContract.Interfaces.ICreate) {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import config from '../configs'
|
||||
import { initExtensionsInPostgres, initSystemStatus } from '../postgres-init'
|
||||
import { installExtraData, installInitialData, startInfra } from './infra'
|
||||
import { CooperativeClass, startCoop } from './cooperative'
|
||||
import { startCoop } from './cooperative'
|
||||
|
||||
export async function boot() {
|
||||
const blockchain = await startInfra()
|
||||
@@ -14,18 +13,29 @@ export async function boot() {
|
||||
}
|
||||
|
||||
export async function bootClean() {
|
||||
const blockchain = await startInfra()
|
||||
|
||||
console.log('Создаём программы (Благорост и маркетплейс)')
|
||||
|
||||
const cooperative = new CooperativeClass(blockchain)
|
||||
await cooperative.createPrograms(config.provider)
|
||||
// Только инфраструктура: контракты, фичи, токен, системные параметры.
|
||||
// Ни совета, ни программ — программы (createPrograms) требуют существующий
|
||||
// совет и создаются в boot/bootExtra внутри installInitialData. В clean их
|
||||
// не делаем (иначе soviet::createprog падает с «Совет не найден»).
|
||||
await startInfra()
|
||||
}
|
||||
|
||||
export async function bootExtra() {
|
||||
const blockchain = await startInfra()
|
||||
await installInitialData(blockchain, true) // Создать расширенный совет (устанавливает статус 'active' в MongoDB)
|
||||
await installExtraData(blockchain) // Добавить дополнительных пайщиков
|
||||
|
||||
// installExtraData регистрирует partner1 как coop с auto-approve от провайдера —
|
||||
// это и есть программная on-chain активация, которая триггерит аренду VM в
|
||||
// провайдере (PENDING→RENT). boot:extra используется НЕ только для аренды
|
||||
// сервера (например, просто пересев совета/чейна для других задач), поэтому
|
||||
// провижининг partner1 включается ТОЛЬКО под флагом EXTRA_RENT=1.
|
||||
if (process.env.EXTRA_RENT === '1') {
|
||||
console.log('EXTRA_RENT=1 → installExtraData: провижининг partner1 (триггер аренды)')
|
||||
await installExtraData(blockchain) // Регистрируем partner1 как coop (active)
|
||||
}
|
||||
else {
|
||||
console.log('EXTRA_RENT не задан → пропускаем провижининг partner1 (аренда не запускается)')
|
||||
}
|
||||
|
||||
console.log('Инициализируем статус системы в PostgreSQL')
|
||||
await initSystemStatus() // Устанавливает статус 'active' в PostgreSQL
|
||||
|
||||
@@ -15,6 +15,9 @@ import { sleep } from '../utils'
|
||||
import { generateRandomSHA256 } from '../utils/randomHash'
|
||||
import { initUsersInPostgres, initVaultInPostgres } from '../postgres-init'
|
||||
import { CooperativeClass } from './cooperative'
|
||||
import { signProgramAgreement } from './sign-program-agreement'
|
||||
import { fakeDocument } from '../tests/shared/fakeDocument'
|
||||
import { walletDraftId, walletProgramId } from '../tests/capital/consts'
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url)
|
||||
const __dirname = dirname(__filename)
|
||||
@@ -674,10 +677,17 @@ export async function installExtraData(blockchain: Blockchain) {
|
||||
status: 'active',
|
||||
})
|
||||
|
||||
// Ресурсы для партнёра: powerup CPU/NET. transfer токенов пропускаем —
|
||||
// eosio.token::transfer падает на проверке membership в wallet program,
|
||||
// которая для partner1 не настроена. Для provider sync + Hostkey-flow
|
||||
// токены не нужны: tx от имени partner1 пойдут от soviet/admin.
|
||||
// Записываем partner1 в ЦПП Кошелька оператора (voskhod, program_id=1).
|
||||
// БЕЗ этого провайдерский performInitialTransfers (150 AXON на partner1 ДО
|
||||
// перехода в ACTIVE, см. provider CLAUDE.md §13) падает ассертом
|
||||
// eosio.token::is_can_transfer «Получатель не является участником целевой
|
||||
// потребительской программы кошелька» — и инстанс навсегда застревает в
|
||||
// INSTALL. wallet::signagree (auth voskhod@active) делает partner1 членом
|
||||
// ЦПП Кошелька. Та же механика, что для обычных пайщиков в participant.ts.
|
||||
console.log('Подписываем wallet-соглашение за partner1 (членство в ЦПП Кошелька)')
|
||||
await signProgramAgreement(blockchain, config.provider, account.username, walletProgramId, walletDraftId, fakeDocument)
|
||||
|
||||
// Ресурсы для партнёра: powerup CPU/NET.
|
||||
await blockchain.powerup({
|
||||
payer: 'eosio',
|
||||
receiver: account.username,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@coopenomics/cleos",
|
||||
"version": "2026.5.23-4",
|
||||
"version": "2026.5.25-3",
|
||||
"private": true,
|
||||
"description": "Обёртка над кошельком cleos для EOSIO блокчейна",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
#!/usr/bin/env bash
|
||||
# Локальная сборка всех контрактов: оборачивает build_contracts_cdt.sh в
|
||||
# CDT-образ. Сам cmake/make живёт в build_contracts_cdt.sh — он же
|
||||
# используется в CI (там CDT ставится из .deb, без docker), поэтому
|
||||
# флаги сборки в одном месте и не разъезжаются.
|
||||
|
||||
mode="${1:-prod}"
|
||||
|
||||
if [ "$mode" = "test" ]; then
|
||||
is_testnet="ON"
|
||||
else
|
||||
is_testnet="OFF"
|
||||
fi
|
||||
|
||||
docker run --rm --name cdt \
|
||||
--volume "$(pwd):/project" \
|
||||
-w /project \
|
||||
dicoop/blockchain:latest \
|
||||
/bin/bash -c "mkdir -p build && cd build && cmake -DBUILD_TARGET= -DTEST_TARGET= -DVERBOSE=ON -DBUILD_TESTS=OFF -DIS_TESTNET=$is_testnet .. && make"
|
||||
/bin/bash -c "./build_contracts_cdt.sh $mode"
|
||||
|
||||
Executable
+28
@@ -0,0 +1,28 @@
|
||||
#!/usr/bin/env bash
|
||||
# Компиляция всех контрактов кооперативной экономики через CDT.
|
||||
#
|
||||
# ВАЖНО: скрипт запускается УЖЕ внутри окружения с установленным CDT —
|
||||
# тулчейн ожидается по пути /cdt/build/lib/cmake/cdt/CDTWasmToolchain.cmake
|
||||
# (он захардкожен в CMakeLists.txt). Здесь НЕТ docker — только cmake/make.
|
||||
#
|
||||
# Два способа подготовить это окружение:
|
||||
# - локально: build-all.sh оборачивает скрипт в `docker run dicoop/blockchain:latest`
|
||||
# (в образе CDT уже лежит в /cdt/build);
|
||||
# - в CI: CDT ставится из .deb (C9S/cdt), а /cdt/build симлинкуется на
|
||||
# /usr/opt/cdt/<ver> (см. .github/workflows/release.yaml).
|
||||
#
|
||||
# Один источник cmake-флагов для обоих путей — чтобы локальная и CI-сборка
|
||||
# не разъезжались.
|
||||
set -euo pipefail
|
||||
|
||||
mode="${1:-prod}"
|
||||
if [ "$mode" = "test" ]; then
|
||||
is_testnet="ON"
|
||||
else
|
||||
is_testnet="OFF"
|
||||
fi
|
||||
|
||||
mkdir -p build
|
||||
cd build
|
||||
cmake -DBUILD_TARGET= -DTEST_TARGET= -DVERBOSE=ON -DBUILD_TESTS=OFF -DIS_TESTNET="$is_testnet" ..
|
||||
make
|
||||
+2
-2
@@ -72,8 +72,8 @@ void capital::importcontrib(eosio::name coopname, eosio::name username, checksum
|
||||
auto idx = user_wallets.get_index<"byuserwallet"_n>();
|
||||
auto preimp_it = idx.find(combine_ids(ledger2_wallets::PREIMP_FUND.value, username.value));
|
||||
if (preimp_it != idx.end() && preimp_it->available.amount > 0) {
|
||||
eosio::check(preimp_it->blocked.amount == 0,
|
||||
"preimp.blocked > 0 не поддерживается при импорте — обратиться в поддержку");
|
||||
// Поле blocked упразднено (2026-05-24): резерв/блокировка на кошельках
|
||||
// больше не используется, проверка preimp.blocked == 0 излишня.
|
||||
Ledger2::apply(
|
||||
_capital, coopname, operations::capital::DROP_PREIMP,
|
||||
preimp_it->available, username, contributor_hash,
|
||||
|
||||
@@ -9,23 +9,26 @@ namespace Capital::Core {
|
||||
* @brief Совокупный баланс программы Благорост на уровне кооператива.
|
||||
*
|
||||
* Источник — ledger2 L2 (`wallets[BLAGOROST_FUND]`), агрегированный по всем
|
||||
* пайщикам. Σ L3.{available, blocked}[w.cap.blago] == L2.{available, blocked}
|
||||
* (Эпик 3 invariant), поэтому L2 — авторитетная сумма для CRPS-знаменателя.
|
||||
* Если кошелёк ещё не создан (никто не инвестировал) — нулевой баланс.
|
||||
* пайщикам. Σ L3.available[w.cap.blago] == L2.available (Эпик 3 invariant),
|
||||
* поэтому L2 — авторитетная сумма для CRPS-знаменателя. Если кошелёк ещё не
|
||||
* создан (никто не инвестировал) — нулевой баланс.
|
||||
*
|
||||
* Поле blocked упразднено (2026-05-24): субсчёт «заблокировано» больше не
|
||||
* используется, баланс кошелька = available.
|
||||
*/
|
||||
eosio::asset get_capital_program_share_balance(eosio::name coopname) {
|
||||
wallets2_index wallets(_ledger2, coopname.value);
|
||||
auto it = wallets.find(ledger2_wallets::BLAGOROST_FUND.value);
|
||||
if (it == wallets.end()) return eosio::asset(0, _root_govern_symbol);
|
||||
return it->available + it->blocked;
|
||||
return it->available;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Доля пайщика в Благоросте (available + blocked) из ledger2 L3.
|
||||
* @brief Доля пайщика в Благоросте (available) из ledger2 L3.
|
||||
*
|
||||
* Источник — `userwallets[(w.cap.blago, username)]`. Запись существует
|
||||
* только пока available + blocked > 0 (Эпик 3 §6: auto-delete на нуле),
|
||||
* поэтому отсутствие = нулевой баланс.
|
||||
* только пока available > 0 (Эпик 3 §6: auto-delete на нуле), поэтому
|
||||
* отсутствие = нулевой баланс. Поле blocked упразднено (2026-05-24).
|
||||
*/
|
||||
eosio::asset get_capital_program_user_share_balance(eosio::name coopname, eosio::name username) {
|
||||
userwallets_index user_wallets(_ledger2, coopname.value);
|
||||
@@ -33,6 +36,6 @@ namespace Capital::Core {
|
||||
auto key = combine_ids(ledger2_wallets::BLAGOROST_FUND.value, username.value);
|
||||
auto it = idx.find(key);
|
||||
if (it == idx.end()) return eosio::asset(0, _root_govern_symbol);
|
||||
return it->available + it->blocked;
|
||||
return it->available;
|
||||
}
|
||||
}
|
||||
@@ -67,12 +67,12 @@ void ledger2::revert(eosio::name coopname,
|
||||
eosio::check(original_operation_id != 0, "revert: original_operation_id обязателен");
|
||||
|
||||
// -------- validate mirror_wallet_op --------
|
||||
eosio::check(mirror_wallet_op <= 5, "revert: неизвестный mirror_wallet_op");
|
||||
// Не позволяем откатывать через BLOCK/UNBLOCK — они асимметричны и нет
|
||||
// адекватного зеркала в одной операции (BLOCK + UNBLOCK — обратные сами по себе).
|
||||
eosio::check(mirror_wallet_op != static_cast<uint8_t>(WalletOp::BLOCK) &&
|
||||
mirror_wallet_op != static_cast<uint8_t>(WalletOp::UNBLOCK),
|
||||
"revert: BLOCK/UNBLOCK не подлежат откату через revert (они симметричны сами себе)");
|
||||
// Зеркало revert — только TRANSFER (обмен wallet_from/wallet_to) либо BURN
|
||||
// (зеркало ISSUE: изъятие с wallet_from). Бывшие BLOCK/UNBLOCK упразднены
|
||||
// (2026-05-24); ISSUE/NONE как зеркало смысла не имеют.
|
||||
eosio::check(mirror_wallet_op == static_cast<uint8_t>(WalletOp::TRANSFER) ||
|
||||
mirror_wallet_op == static_cast<uint8_t>(WalletOp::BURN),
|
||||
"revert: mirror_wallet_op должен быть TRANSFER или BURN");
|
||||
|
||||
// -------- validate mirror wallets/accounts --------
|
||||
if (mirror_wallet_from.value != 0) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* @brief Атомарная операция по кошельку (issue/transfer/block/unblock/burn/burn_blocked).
|
||||
* @brief Атомарная операция по кошельку (issue/transfer/burn).
|
||||
*
|
||||
* Внутренний action ledger2 — вызывается только через inline из apply().
|
||||
* Auth: только сам ledger2 (require_auth(get_self())).
|
||||
@@ -53,13 +53,15 @@ void ledger2::walletop(eosio::name coopname,
|
||||
eosio::check(amount.symbol == _root_govern_symbol,
|
||||
"walletop: некорректный символ валюты");
|
||||
eosio::check(memo.size() < 256, "walletop: memo > 255");
|
||||
// op_code = 5 (NONE) намеренно не допускается: NONE-операции — это только
|
||||
// бухпроводка без кошелькового движения, apply.cpp не диспатчит для них walletop.
|
||||
// Прямой вызов с op_code=5 был бы no-op и сбил бы инвариант parity.
|
||||
// Допустимы 0..4 (ISSUE/TRANSFER/BLOCK/UNBLOCK/BURN) и 6 (BURN_BLOCKED).
|
||||
eosio::check(op_code <= static_cast<uint8_t>(WalletOp::BURN_BLOCKED) &&
|
||||
op_code != static_cast<uint8_t>(WalletOp::NONE),
|
||||
"walletop: неизвестный op_code");
|
||||
// Допустимы только ISSUE(0), TRANSFER(1), BURN(4). NONE(5) — это только
|
||||
// бухпроводка без кошелькового движения, apply.cpp для неё walletop не диспатчит;
|
||||
// прямой вызов был бы no-op и сбил бы parity. Бывшие BLOCK(2)/UNBLOCK(3)/
|
||||
// BURN_BLOCKED(6) упразднены (2026-05-24): резерв возврата теперь TRANSFER на
|
||||
// w.wal.wpend, их op_code больше не валиден.
|
||||
eosio::check(op_code == static_cast<uint8_t>(WalletOp::ISSUE) ||
|
||||
op_code == static_cast<uint8_t>(WalletOp::TRANSFER) ||
|
||||
op_code == static_cast<uint8_t>(WalletOp::BURN),
|
||||
"walletop: недопустимый op_code");
|
||||
|
||||
wallets2_index wallets(get_self(), coopname.value);
|
||||
userwallets_index user_wallets(get_self(), coopname.value);
|
||||
@@ -172,7 +174,7 @@ void ledger2::walletop(eosio::name coopname,
|
||||
// миграций 048/049.
|
||||
//
|
||||
// walletop по построению применяет одно и то же `amount` к L2 и L3 (см.
|
||||
// ниже case'ы ISSUE/TRANSFER/BLOCK/UNBLOCK/BURN/BURN_BLOCKED), а sender-guard на
|
||||
// ниже case'ы ISSUE/TRANSFER/BURN), а sender-guard на
|
||||
// строке 46-47 запрещает обход. Поэтому инвариант сохраняется по
|
||||
// конструкции; полную сверку выполняет бэкенд («стол бухгалтера»),
|
||||
// вне транзакционного hot path.
|
||||
@@ -227,60 +229,6 @@ void ledger2::walletop(eosio::name coopname,
|
||||
cleanup_l3_if_empty(wallet_from);
|
||||
break;
|
||||
}
|
||||
case WalletOp::BLOCK: {
|
||||
eosio::check(wallet_from.value != 0, "walletop BLOCK: требуется wallet_from");
|
||||
eosio::check(wallet_to.value == 0, "walletop BLOCK: wallet_to должен быть пустым");
|
||||
|
||||
auto it = wallets.find(wallet_from.value);
|
||||
eosio::check(it != wallets.end() && it->available >= amount,
|
||||
std::string{"walletop BLOCK: недостаточно available на кошельке "} +
|
||||
wallet_from.to_string());
|
||||
wallets.modify(it, payer, [&](auto& w) {
|
||||
w.available -= amount;
|
||||
w.blocked += amount;
|
||||
});
|
||||
|
||||
if (is_user_shared_l3(wallet_from)) {
|
||||
auto uw = find_l3(wallet_from);
|
||||
eosio::check(uw != user_wallets.get_index<"byuserwallet"_n>().end() &&
|
||||
uw->available >= amount,
|
||||
std::string{"walletop BLOCK: недостаточно L3-available у пайщика "} +
|
||||
username.to_string() + " на " + wallet_from.to_string());
|
||||
auto uw_pri = user_wallets.find(uw->id);
|
||||
user_wallets.modify(uw_pri, payer, [&](auto& r) {
|
||||
r.available -= amount;
|
||||
r.blocked += amount;
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
case WalletOp::UNBLOCK: {
|
||||
eosio::check(wallet_from.value != 0, "walletop UNBLOCK: требуется wallet_from");
|
||||
eosio::check(wallet_to.value == 0, "walletop UNBLOCK: wallet_to должен быть пустым");
|
||||
|
||||
auto it = wallets.find(wallet_from.value);
|
||||
eosio::check(it != wallets.end() && it->blocked >= amount,
|
||||
std::string{"walletop UNBLOCK: недостаточно blocked на кошельке "} +
|
||||
wallet_from.to_string());
|
||||
wallets.modify(it, payer, [&](auto& w) {
|
||||
w.blocked -= amount;
|
||||
w.available += amount;
|
||||
});
|
||||
|
||||
if (is_user_shared_l3(wallet_from)) {
|
||||
auto uw = find_l3(wallet_from);
|
||||
eosio::check(uw != user_wallets.get_index<"byuserwallet"_n>().end() &&
|
||||
uw->blocked >= amount,
|
||||
std::string{"walletop UNBLOCK: недостаточно L3-blocked у пайщика "} +
|
||||
username.to_string() + " на " + wallet_from.to_string());
|
||||
auto uw_pri = user_wallets.find(uw->id);
|
||||
user_wallets.modify(uw_pri, payer, [&](auto& r) {
|
||||
r.blocked -= amount;
|
||||
r.available += amount;
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
case WalletOp::BURN: {
|
||||
eosio::check(wallet_from.value != 0, "walletop BURN: требуется wallet_from");
|
||||
eosio::check(wallet_to.value == 0, "walletop BURN: wallet_to должен быть пустым");
|
||||
@@ -311,30 +259,6 @@ void ledger2::walletop(eosio::name coopname,
|
||||
eosio::check(false, "walletop NONE: запрещённый op_code");
|
||||
break;
|
||||
}
|
||||
case WalletOp::BURN_BLOCKED: {
|
||||
eosio::check(wallet_from.value != 0, "walletop BURN_BLOCKED: требуется wallet_from");
|
||||
eosio::check(wallet_to.value == 0, "walletop BURN_BLOCKED: wallet_to должен быть пустым");
|
||||
|
||||
auto it = wallets.find(wallet_from.value);
|
||||
eosio::check(it != wallets.end() && it->blocked >= amount,
|
||||
std::string{"walletop BURN_BLOCKED: недостаточно blocked на кошельке "} +
|
||||
wallet_from.to_string());
|
||||
wallets.modify(it, payer, [&](auto& w) { w.blocked -= amount; });
|
||||
|
||||
if (is_user_shared_l3(wallet_from)) {
|
||||
auto uw = find_l3(wallet_from);
|
||||
eosio::check(uw != user_wallets.get_index<"byuserwallet"_n>().end() &&
|
||||
uw->blocked >= amount,
|
||||
std::string{"walletop BURN_BLOCKED: недостаточно L3-blocked у пайщика "} +
|
||||
username.to_string() + " на " + wallet_from.to_string());
|
||||
auto uw_pri = user_wallets.find(uw->id);
|
||||
user_wallets.modify(uw_pri, payer, [&](auto& r) { r.blocked -= amount; });
|
||||
}
|
||||
|
||||
cleanup_l2_if_empty(wallet_from);
|
||||
cleanup_l3_if_empty(wallet_from);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Post-mutation Σ L3 == L2: см. блок выше (строки 163-175). Снято
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
#include <vector>
|
||||
|
||||
/**
|
||||
* @brief Универсальное миграционное действие контракта ledger2 — точка
|
||||
* расширения для разовых исправлений состояния, которые можно провести
|
||||
@@ -9,23 +7,33 @@
|
||||
* после её прогона на проде тело очищается до пустого `require_auth(get_self())`
|
||||
* (как в `capital::migrate`).
|
||||
*
|
||||
* Текущая задача (2026-05-21): чистка осиротевших L3-записей `w.cap.gen`
|
||||
* на voskhod после смены WalletKind GENERATOR_FUND с USER_SHARED на
|
||||
* COOPERATIVE (см. `lib/core/ledger2/wallets.hpp`).
|
||||
* Текущая задача (2026-05-24): свёртка `blocked → available` по ВСЕМ коопам.
|
||||
*
|
||||
* Контекст: на voskhod `convertsegm` падал «недостаточно L3-средств у
|
||||
* пайщика» в проектах, где CRPS перераспределял доли между сегментами.
|
||||
* w.cap.gen был USER_SHARED, а CRPS в approvecmmt не делал per-user
|
||||
* компенсирующих TRANSFER между сегментами: `Σ COMMIT_RID == Σ ACCEPT_RID`
|
||||
* соблюдался только на проекте, не на пайщике.
|
||||
* Контекст: механика «заблокированного» баланса упразднена (см.
|
||||
* `lib/core/ledger2/operations.hpp` — удалены WalletOp BLOCK/UNBLOCK/BURN_BLOCKED;
|
||||
* резерв возврата паевого теперь выражается переводом на кошелёк-резерв
|
||||
* `w.wal.wpend`). Поле `blocked` остаётся в таблицах `wallets2`/`userwallets`
|
||||
* как deprecated (физическое удаление поля = небезопасная смена layout таблицы
|
||||
* на живых коопах, выносится в отдельный cleanup-деплой). Перед тем как поле
|
||||
* перестанет поддерживаться кодом, накопленные `blocked`-остатки нужно вернуть
|
||||
* в `available`, чтобы средства не «зависли» на упразднённом субсчёте.
|
||||
*
|
||||
* Архитектурный фикс: w.cap.gen — COOPERATIVE-пул без L3. Чтобы UI/бэкенд
|
||||
* не врали остатками по «личным» w.cap.gen, удаляем осиротевшие L3-записи
|
||||
* прямым `userwallets.erase`. L2-баланс `wallets2[w.cap.gen]` уже верен
|
||||
* (синхронен с Σ старых userwallets), его не трогаем.
|
||||
* Действие: пройти всех кооперативов (cooperatives2 в scope registrator) и для
|
||||
* каждого свернуть `blocked → available` на уровнях L2 (`wallets2`) и L3
|
||||
* (`userwallets`): `available += blocked; blocked = 0`. Сумма средств на кошельке
|
||||
* не меняется — только субсчёт.
|
||||
*
|
||||
* Идемпотентно: повторный вызов на чистой БД — no-op (lower_bound пуст).
|
||||
* Только voskhod: на остальных кооперативах w.cap.gen не использовался.
|
||||
* Идемпотентно: после свёртки `blocked == 0`, повторный прогон — no-op.
|
||||
*
|
||||
* Сигнатура без аргументов — действие вызывается автоматически при деплое
|
||||
* контракта (как и прочие задачи migrate); проходит по всем кооперативам сам.
|
||||
*
|
||||
* ПРЕДУСЛОВИЕ (операционное): на момент прогона не должно быть заявок на возврат
|
||||
* «в полёте» (статусы pending/authorized в `wallet::withdraws`) — их `blocked`
|
||||
* относится к старой механике и при свёртке в `available` вернётся пайщику как
|
||||
* свободные средства, а последующий `completewthd` (BURN с `w.wal.wpend`) не
|
||||
* найдёт резерва. Незавершённые заявки нужно довести (complete/decline) ДО
|
||||
* деплоя с этой миграцией.
|
||||
*
|
||||
* @ingroup public_ledger2_actions
|
||||
*
|
||||
@@ -34,24 +42,31 @@
|
||||
void ledger2::migrate() {
|
||||
require_auth(get_self());
|
||||
|
||||
const eosio::name target_coop = "voskhod"_n;
|
||||
cooperatives2_index coops(_registrator, _registrator.value);
|
||||
|
||||
userwallets_index user_wallets(get_self(), target_coop.value);
|
||||
auto idx = user_wallets.get_index<"bywallet"_n>();
|
||||
for (auto c = coops.begin(); c != coops.end(); ++c) {
|
||||
const eosio::name coopname = c->username;
|
||||
|
||||
// Собираем primary id записей до erase (модификация контейнера при
|
||||
// итерации через secondary index — небезопасна).
|
||||
std::vector<uint64_t> ids_to_erase;
|
||||
for (auto it = idx.lower_bound(ledger2_wallets::GENERATOR_FUND.value);
|
||||
it != idx.end() && it->wallet_name == ledger2_wallets::GENERATOR_FUND;
|
||||
++it) {
|
||||
ids_to_erase.push_back(it->id);
|
||||
}
|
||||
// --- L3: userwallets[coopname] ---
|
||||
// Модифицируем только не-ключевые поля (available/blocked) — итерация по
|
||||
// первичному индексу с modify безопасна (порядок строк не меняется).
|
||||
userwallets_index user_wallets(get_self(), coopname.value);
|
||||
for (auto it = user_wallets.begin(); it != user_wallets.end(); ++it) {
|
||||
if (it->blocked.amount <= 0) continue;
|
||||
user_wallets.modify(it, get_self(), [&](auto& r) {
|
||||
r.available += r.blocked;
|
||||
r.blocked = eosio::asset(0, r.blocked.symbol);
|
||||
});
|
||||
}
|
||||
|
||||
for (uint64_t id : ids_to_erase) {
|
||||
auto pri = user_wallets.find(id);
|
||||
if (pri != user_wallets.end()) {
|
||||
user_wallets.erase(pri);
|
||||
// --- L2: wallets2[coopname] ---
|
||||
wallets2_index wallets(get_self(), coopname.value);
|
||||
for (auto it = wallets.begin(); it != wallets.end(); ++it) {
|
||||
if (it->blocked.amount <= 0) continue;
|
||||
wallets.modify(it, get_self(), [&](auto& w) {
|
||||
w.available += w.blocked;
|
||||
w.blocked = eosio::asset(0, w.blocked.symbol);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,9 +31,9 @@
|
||||
* - `WalletOp::WALLET_ONLY` удалён (ADR-003): «без бухпроводок» определяется
|
||||
* парой `(debit_account_id == 0, credit_account_id == 0)` на уровне записи.
|
||||
* Для `o.cap.invest` теперь TRANSFER без проводок (оба account_id == 0).
|
||||
* - Добавлен `WalletOp::BURN` (ADR-003): `available -= amount` на `wallet_from`,
|
||||
* без `wallet_to`. На текущем этапе в `OPERATION_REGISTRY` не используется —
|
||||
* зарезервирован под будущие операции штатного сжигания.
|
||||
* - `WalletOp::BURN` (ADR-003): `available -= amount` на `wallet_from`,
|
||||
* без `wallet_to`. Используется в `o.wal.wthcpl` (сжигание резерва возврата
|
||||
* с `w.wal.wpend`), `o.cap.drppre` и как зеркало ISSUE в `revert`.
|
||||
* - Единые программные кошельки: `BLAGOROST_FUND` (`w.cap.blago`) и
|
||||
* `GENERATOR_FUND` (`w.cap.gen`) — заменили ранее декомпозированные
|
||||
* `bginv/bgprop/bgrid/bgmem` и `gncom/gnmem` (ADR-009).
|
||||
@@ -61,9 +61,9 @@ namespace operations {
|
||||
// wallet
|
||||
namespace wallet {
|
||||
inline constexpr eosio::name COMPLETE_DEPOSIT = "o.wal.depcpl"_n; ///< Завершение внесения паевого взноса (Dr 51 / Cr 80, ISSUE SHARE_FUND_PAY).
|
||||
inline constexpr eosio::name COMPLETE_WITHDRAW = "o.wal.wthcpl"_n; ///< Завершение возврата паевого взноса (Dr 80 / Cr 51, BURN_BLOCKED SHARE_FUND_PAY — деньги уходят из системы, без wallet_to).
|
||||
inline constexpr eosio::name REQUEST_WITHDRAW = "o.wal.wthreq"_n; ///< Запрос на возврат паевого: BLOCK на SHARE_FUND_PAY (без Dr/Cr).
|
||||
inline constexpr eosio::name DECLINE_WITHDRAW = "o.wal.wthdec"_n; ///< Отклонение запроса на возврат: UNBLOCK на SHARE_FUND_PAY (без Dr/Cr).
|
||||
inline constexpr eosio::name COMPLETE_WITHDRAW = "o.wal.wthcpl"_n; ///< Завершение возврата паевого взноса (Dr 80 / Cr 51, BURN с WITHDRAW_PENDING — деньги уходят из системы, без wallet_to).
|
||||
inline constexpr eosio::name REQUEST_WITHDRAW = "o.wal.wthreq"_n; ///< Запрос на возврат паевого: TRANSFER SHARE_FUND_PAY → WITHDRAW_PENDING (резерв, без Dr/Cr).
|
||||
inline constexpr eosio::name DECLINE_WITHDRAW = "o.wal.wthdec"_n; ///< Отклонение запроса на возврат: TRANSFER WITHDRAW_PENDING → SHARE_FUND_PAY (снятие резерва, без Dr/Cr).
|
||||
}
|
||||
|
||||
// capital
|
||||
@@ -129,14 +129,23 @@ namespace operations {
|
||||
* `(debit_account_id == 0, credit_account_id == 0)` на уровне записи реестра.
|
||||
* Compile-time правило `(debit==0) ⇔ (credit==0)` ловит смешанные пары.
|
||||
*/
|
||||
//
|
||||
// Удаление BLOCK/UNBLOCK/BURN_BLOCKED (2026-05-24): механика «заблокированного»
|
||||
// баланса упразднена. Резерв средств под заявку на возврат паевого теперь
|
||||
// выражается переводом на отдельный кошелёк-резерв `w.wal.wpend` (TRANSFER),
|
||||
// возврат резерва — обратным TRANSFER, завершение — BURN с резерва. Поле
|
||||
// `blocked` в таблицах wallets2/userwallets оставлено deprecated (всегда 0
|
||||
// после ledger2::migrate-свёртки) — физическое удаление поля = небезопасная
|
||||
// смена layout таблицы на живых коопах, выносится в отдельный cleanup-деплой.
|
||||
//
|
||||
// Числовые значения ISSUE/TRANSFER/BURN/NONE СОХРАНЕНЫ (не перенумерованы),
|
||||
// чтобы исторические op_code в blockchain_actions читались бэкендом без сдвига
|
||||
// смысла (2 и 3 — бывшие BLOCK/UNBLOCK — больше не выдаются и невалидны на входе).
|
||||
enum class WalletOp : uint8_t {
|
||||
ISSUE = 0, ///< первичный вход средств на кошелёк wallet_to (wallet_from = empty)
|
||||
TRANSFER = 1, ///< перемещение wallet_from → wallet_to (с Dr/Cr ИЛИ без — по парам account_id)
|
||||
BLOCK = 2, ///< available-=amount, blocked+=amount на wallet_from
|
||||
UNBLOCK = 3, ///< blocked-=amount, available+=amount на wallet_from
|
||||
BURN = 4, ///< изъятие amount с wallet_from->available, без wallet_to. Покрывает оба кейса: (a) штатное сжигание как бизнес-операция в OPERATION_REGISTRY; (b) зеркало ISSUE при `ledger2::revert` (различие — через operation_code: `o.adj.rev` для adjustment-mirror).
|
||||
NONE = 5, ///< только бухпроводка без перемещения средств (wallet_from = empty, wallet_to = empty, debit ≠ 0, credit ≠ 0). Покрывает кейсы внутрибалансовых проводок типа Dr 04 / Cr 08 (приём РИД в НМА), когда кошелёк уже на нужном программном фонде.
|
||||
BURN_BLOCKED = 6, ///< изъятие amount из wallet_from->blocked, без wallet_to. Используется при завершении возврата паевого (`o.wal.wthcpl`): средства предварительно заблокированы через BLOCK (REQUEST_WITHDRAW), на завершении сжигаются — выходят из системы (получателя на цепи нет). На L3 синхронно: from_uw->blocked -= amount.
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -145,7 +154,7 @@ enum class WalletOp : uint8_t {
|
||||
* Семантика полей по `wallet_op`:
|
||||
* - ISSUE: wallet_from = eosio::name{}, wallet_to = required.
|
||||
* - TRANSFER: wallet_from = required, wallet_to = required (≠ from).
|
||||
* - BLOCK / UNBLOCK / BURN / BURN_BLOCKED: wallet_from = required, wallet_to = eosio::name{}.
|
||||
* - BURN: wallet_from = required, wallet_to = eosio::name{}.
|
||||
*
|
||||
* Семантика бух.проводки:
|
||||
* - Без проводок: debit_account_id == 0 И credit_account_id == 0.
|
||||
@@ -157,7 +166,7 @@ struct OperationRegistryEntry {
|
||||
eosio::name process_type; ///< тип процесса с префиксом `p.<contract>.<noun>`
|
||||
WalletOp wallet_op;
|
||||
eosio::name wallet_from; ///< пустое имя для ISSUE
|
||||
eosio::name wallet_to; ///< пустое имя для BLOCK/UNBLOCK/BURN/BURN_BLOCKED
|
||||
eosio::name wallet_to; ///< пустое имя для BURN
|
||||
uint64_t debit_account_id; ///< 0 если без бухпроводки (тогда credit_account_id тоже == 0)
|
||||
uint64_t credit_account_id; ///< 0 если без бухпроводки (тогда debit_account_id тоже == 0)
|
||||
const char* human_name;
|
||||
@@ -184,12 +193,12 @@ static constexpr OperationRegistryEntry OPERATION_REGISTRY[] = {
|
||||
ledger2_accounts::BANK_ACCOUNT, ledger2_accounts::SHARE_FUND,
|
||||
"Внесение пайщиком паевого взноса" },
|
||||
|
||||
// 4. Возврат паевого взноса: Dr 80 / Cr 51, BURN_BLOCKED SHARE_FUND_PAY.
|
||||
// Сжигание из заблокированной суммы пайщика (BLOCK был на REQUEST_WITHDRAW):
|
||||
// 4. Возврат паевого взноса: Dr 80 / Cr 51, BURN WITHDRAW_PENDING.
|
||||
// Сжигание из кошелька-резерва (TRANSFER в резерв был на REQUEST_WITHDRAW):
|
||||
// деньги уходят из системы (банковский перевод пайщику), получателя на цепи нет.
|
||||
// Бухгалтерия: паевой фонд уменьшается (Дт 80), расчётный счёт уменьшается (Кт 51).
|
||||
{ operations::wallet::COMPLETE_WITHDRAW, processes::wallet::WITHDRAW, WalletOp::BURN_BLOCKED,
|
||||
ledger2_wallets::SHARE_FUND_PAY, eosio::name{},
|
||||
{ operations::wallet::COMPLETE_WITHDRAW, processes::wallet::WITHDRAW, WalletOp::BURN,
|
||||
ledger2_wallets::WITHDRAW_PENDING, eosio::name{},
|
||||
ledger2_accounts::SHARE_FUND, ledger2_accounts::BANK_ACCOUNT,
|
||||
"Возврат паевого взноса пайщику" },
|
||||
|
||||
@@ -273,17 +282,19 @@ static constexpr OperationRegistryEntry OPERATION_REGISTRY[] = {
|
||||
ledger2_accounts::SHARE_FUND, ledger2_accounts::TARGET_RECEIPTS,
|
||||
"Трансляция паевого взноса из ЦПП «Цифровой Кошелёк» в членский взнос за пользование инфраструктурой" },
|
||||
|
||||
// 15. Запрос на возврат паевого: BLOCK SHARE_FUND_PAY (без Dr/Cr — внутри одного бухсчёта 80).
|
||||
{ operations::wallet::REQUEST_WITHDRAW, processes::wallet::WITHDRAW, WalletOp::BLOCK,
|
||||
ledger2_wallets::SHARE_FUND_PAY, eosio::name{},
|
||||
// 15. Запрос на возврат паевого: TRANSFER SHARE_FUND_PAY → WITHDRAW_PENDING
|
||||
// (без Dr/Cr — оба кошелька на счёте 80; резерв средств на время рассмотрения).
|
||||
{ operations::wallet::REQUEST_WITHDRAW, processes::wallet::WITHDRAW, WalletOp::TRANSFER,
|
||||
ledger2_wallets::SHARE_FUND_PAY, ledger2_wallets::WITHDRAW_PENDING,
|
||||
0, 0,
|
||||
"Блокировка паевого под запрос на возврат" },
|
||||
"Резервирование паевого под запрос на возврат" },
|
||||
|
||||
// 16. Отклонение запроса на возврат: UNBLOCK SHARE_FUND_PAY (без Dr/Cr — зеркало REQUEST_WITHDRAW).
|
||||
{ operations::wallet::DECLINE_WITHDRAW, processes::wallet::WITHDRAW, WalletOp::UNBLOCK,
|
||||
ledger2_wallets::SHARE_FUND_PAY, eosio::name{},
|
||||
// 16. Отклонение запроса на возврат: TRANSFER WITHDRAW_PENDING → SHARE_FUND_PAY
|
||||
// (без Dr/Cr — зеркало REQUEST_WITHDRAW; возврат резерва пайщику).
|
||||
{ operations::wallet::DECLINE_WITHDRAW, processes::wallet::WITHDRAW, WalletOp::TRANSFER,
|
||||
ledger2_wallets::WITHDRAW_PENDING, ledger2_wallets::SHARE_FUND_PAY,
|
||||
0, 0,
|
||||
"Разблокировка паевого после отклонения запроса на возврат" },
|
||||
"Снятие резерва паевого после отклонения запроса на возврат" },
|
||||
|
||||
// 17. Возврат из ЦПП «Благорост» в Цифровой Кошелёк: TRANSFER BLAGOROST_FUND → SHARE_FUND_PAY (без Dr/Cr — оба счёта 80, зеркало INVEST).
|
||||
{ operations::capital::WITHDRAW_FROM_CAPITAL, processes::capital::WTHCAP, WalletOp::TRANSFER,
|
||||
@@ -340,7 +351,7 @@ static constexpr size_t OPERATION_REGISTRY_SIZE = sizeof(OPERATION_REGISTRY) / s
|
||||
// оба существуют в `LEDGER2_ACCOUNT_MAP`.
|
||||
// 4. Для TRANSFER: `wallet_from` ≠ `wallet_to`, оба ≠ 0.
|
||||
// 5. Для ISSUE: `wallet_from` == 0 и `wallet_to` ≠ 0.
|
||||
// 6. Для BLOCK / UNBLOCK / BURN / BURN_BLOCKED: `wallet_from` ≠ 0, `wallet_to` == 0.
|
||||
// 6. Для BURN: `wallet_from` ≠ 0, `wallet_to` == 0.
|
||||
// 7. Все id кошельков из записей существуют в `LEDGER2_WALLET_REGISTRY`.
|
||||
namespace ledger2_registry_detail {
|
||||
constexpr bool operation_codes_unique() {
|
||||
@@ -384,11 +395,11 @@ namespace ledger2_registry_detail {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Правило 6: BURN / BURN_BLOCKED — wallet_from required, wallet_to == 0 (ADR-003).
|
||||
// Правило 6: BURN — wallet_from required, wallet_to == 0 (ADR-003).
|
||||
constexpr bool burn_pattern_correct() {
|
||||
for (size_t i = 0; i < OPERATION_REGISTRY_SIZE; ++i) {
|
||||
const auto& e = OPERATION_REGISTRY[i];
|
||||
if (e.wallet_op != WalletOp::BURN && e.wallet_op != WalletOp::BURN_BLOCKED) continue;
|
||||
if (e.wallet_op != WalletOp::BURN) continue;
|
||||
if (e.wallet_from.value == 0) return false;
|
||||
if (e.wallet_to.value != 0) return false;
|
||||
}
|
||||
@@ -440,7 +451,7 @@ static_assert(ledger2_registry_detail::dr_ne_cr_when_posting(),
|
||||
static_assert(ledger2_registry_detail::transfer_wallet_from_ne_to(),
|
||||
"OPERATION_REGISTRY: TRANSFER с wallet_from == wallet_to или одним из них == 0");
|
||||
static_assert(ledger2_registry_detail::burn_pattern_correct(),
|
||||
"OPERATION_REGISTRY: BURN / BURN_BLOCKED требует wallet_from ≠ 0 и wallet_to == 0");
|
||||
"OPERATION_REGISTRY: BURN требует wallet_from ≠ 0 и wallet_to == 0");
|
||||
static_assert(ledger2_registry_detail::none_pattern_correct(),
|
||||
"OPERATION_REGISTRY: NONE требует wallet_from == 0, wallet_to == 0 и обе проводки заполненными");
|
||||
static_assert(ledger2_registry_detail::accounts_exist_in_map(),
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
* w.mkt.* — Маркетплейс (выплаты поставщикам)
|
||||
*
|
||||
* Sentinel `eosio::name{}` (пустое имя, value=0) — «кошелёк вне системы»
|
||||
* для ISSUE (нет wallet_from) и для BURN/BLOCK/UNBLOCK (нет wallet_to).
|
||||
* для ISSUE (нет wallet_from) и для BURN (нет wallet_to).
|
||||
*
|
||||
* При первом ISSUE/TRANSFER кошелёк создаётся автоматически по записи
|
||||
* из WALLET_REGISTRY. При обнулении available+blocked запись удаляется.
|
||||
@@ -46,7 +46,8 @@ struct ledger2_wallets {
|
||||
// wallet — паевой фонд + возвраты + ЦК
|
||||
static constexpr eosio::name SHARE_FUND_PAY = "w.wal.share"_n; ///< Паевой взнос пайщика (USER_SHARED)
|
||||
static constexpr eosio::name CK_MEMBER = "w.wal.member"_n; ///< ЦК — членская часть пайщика (USER_SHARED)
|
||||
static constexpr eosio::name WITHDRAWALS_SINK = "w.wal.wthdrw"_n; ///< DEPRECATED 2026-05-21: после переключения o.wal.wthcpl с TRANSFER на BURN_BLOCKED кошелёк больше не получает новых средств. Оставлен в реестре для исторических L2-балансов (накопленные возвраты до перехода). Не использовать в новых операциях.
|
||||
static constexpr eosio::name WITHDRAWALS_SINK = "w.wal.wthdrw"_n; ///< DEPRECATED 2026-05-21: исторический sink возвратов. Оставлен в реестре для исторических L2-балансов (накопленные возвраты до перехода). Не использовать в новых операциях.
|
||||
static constexpr eosio::name WITHDRAW_PENDING = "w.wal.wpend"_n; ///< Резерв паевого под заявку на возврат (COOPERATIVE-пул). o.wal.wthreq переводит сюда с w.wal.share, o.wal.wthdec возвращает обратно, o.wal.wthcpl сжигает отсюда. Заменил механику blocked/BLOCK/UNBLOCK 2026-05-24.
|
||||
|
||||
// registrator — минимальный паевой + вступительные
|
||||
static constexpr eosio::name MIN_SHARE_FUND = "w.reg.minshr"_n; ///< Минимальный паевой взнос пайщика (USER_SHARED, без сверки соглашений)
|
||||
@@ -94,7 +95,7 @@ struct Ledger2WalletMeta {
|
||||
WalletKind kind;
|
||||
};
|
||||
|
||||
inline constexpr std::array<Ledger2WalletMeta, 14> LEDGER2_WALLET_REGISTRY = {{
|
||||
inline constexpr std::array<Ledger2WalletMeta, 15> LEDGER2_WALLET_REGISTRY = {{
|
||||
// USER_SHARED (5) — L3-разрез по пайщику
|
||||
{ ledger2_wallets::MIN_SHARE_FUND, "Минимальный паевой взнос", WalletKind::USER_SHARED },
|
||||
{ ledger2_wallets::SHARE_FUND_PAY, "Паевой взнос пайщика", WalletKind::USER_SHARED },
|
||||
@@ -102,7 +103,7 @@ inline constexpr std::array<Ledger2WalletMeta, 14> LEDGER2_WALLET_REGISTRY = {{
|
||||
{ ledger2_wallets::BLAGOROST_FUND, "ЦПП «Благорост» — единый кошелёк программы у пайщика", WalletKind::USER_SHARED },
|
||||
{ ledger2_wallets::PREIMP_FUND, "Первичный учёт РИД-взносов до перехода на электронный учёт", WalletKind::USER_SHARED },
|
||||
|
||||
// COOPERATIVE (9) — единый кооперативный баланс, без L3
|
||||
// COOPERATIVE (10) — единый кооперативный баланс, без L3
|
||||
// GENERATOR_FUND переведён сюда из USER_SHARED (см. wallets.hpp:64) —
|
||||
// CRPS-распределение между сегментами проекта не поддерживает per-user
|
||||
// компенсирующие TRANSFER на approvecmmt, поэтому L3-проверка walletop
|
||||
@@ -110,6 +111,7 @@ inline constexpr std::array<Ledger2WalletMeta, 14> LEDGER2_WALLET_REGISTRY = {{
|
||||
{ ledger2_wallets::GENERATOR_FUND, "ЦПП «Генератор» — единый кошелёк программы", WalletKind::COOPERATIVE },
|
||||
{ ledger2_wallets::ENTRANCE_FEES, "Вступительные взносы", WalletKind::COOPERATIVE },
|
||||
{ ledger2_wallets::WITHDRAWALS_SINK, "Возвраты паевых взносов пайщикам (deprecated, не используется в новых операциях)", WalletKind::COOPERATIVE },
|
||||
{ ledger2_wallets::WITHDRAW_PENDING, "Резерв паевого под заявку на возврат", WalletKind::COOPERATIVE },
|
||||
{ ledger2_wallets::INFRA_FEES, "Членские взносы за инфраструктуру кооп. платформы", WalletKind::COOPERATIVE },
|
||||
{ ledger2_wallets::DELEGATE_FEES, "Делегатские членские взносы", WalletKind::COOPERATIVE },
|
||||
{ ledger2_wallets::SOV_EXPENSES, "Хозяйственные расходы из числа целевого финансирования", WalletKind::COOPERATIVE },
|
||||
|
||||
@@ -3,10 +3,11 @@
|
||||
# программу «Цифровой Кошелёк» (SHARE_FUND_PAY).
|
||||
#
|
||||
# Четырёхактовый процесс с одной авторизацией советом. Запись заявки живёт в
|
||||
# таблице `withdraws` от создания до выплаты или отказа. На закрывающем
|
||||
# действии completewthd срабатывает одна операция o.wal.wthcpl — сжигание
|
||||
# заблокированной суммы (BURN_BLOCKED w.wal.share): деньги уходят из системы
|
||||
# пайщику банковским переводом, получателя на цепи нет.
|
||||
# таблице `withdraws` от создания до выплаты или отказа. На подаче заявки сумма
|
||||
# переводится в кошелёк-резерв (WITHDRAW_PENDING, w.wal.wpend). На закрывающем
|
||||
# действии completewthd срабатывает операция o.wal.wthcpl — сжигание суммы из
|
||||
# резерва (BURN w.wal.wpend): деньги уходят из системы пайщику банковским
|
||||
# переводом, получателя на цепи нет.
|
||||
#
|
||||
# Источники правды в коде:
|
||||
# • cpp/wallet/wallet.hpp — actions
|
||||
@@ -43,8 +44,8 @@ actions:
|
||||
actor: contributor
|
||||
role: opener
|
||||
purpose: >
|
||||
Пайщик создаёт заявку на возврат паевого взноса. Контракт блокирует
|
||||
сумму на кошельке SHARE_FUND_PAY (статус available → blocked) и
|
||||
Пайщик создаёт заявку на возврат паевого взноса. Контракт переводит
|
||||
сумму с кошелька SHARE_FUND_PAY в кошелёк-резерв WITHDRAW_PENDING и
|
||||
создаёт повестку в совете о возврате с callback'ом авторизации.
|
||||
- name: wallet::authwthd
|
||||
human: Авторизовать выплату
|
||||
@@ -60,16 +61,17 @@ actions:
|
||||
role: closer
|
||||
purpose: >
|
||||
Gateway подтвердил списание средств в пользу пайщика. Это закрывающее
|
||||
действие: применяется операция o.wal.wthcpl (BURN_BLOCKED w.wal.share)
|
||||
— заблокированная сумма сжигается, в бухгалтерии проходит обратная
|
||||
действие: применяется операция o.wal.wthcpl (BURN w.wal.wpend)
|
||||
— сумма из резерва сжигается, в бухгалтерии проходит обратная
|
||||
проводка Дт 80 / Кт 51. Запись заявки удаляется.
|
||||
- name: wallet::declinewthd
|
||||
human: Отклонить
|
||||
actor: soviet
|
||||
role: reject
|
||||
purpose: >
|
||||
Отказ на любом из этапов до выплаты. Сумма разблокируется на кошельке
|
||||
пайщика (UNBLOCK через o.wal.wthdec), запись заявки удаляется.
|
||||
Отказ на любом из этапов до выплаты. Сумма возвращается из резерва на
|
||||
кошелёк пайщика (o.wal.wthdec: WITHDRAW_PENDING → SHARE_FUND_PAY),
|
||||
запись заявки удаляется.
|
||||
|
||||
# ── Секция 3. Граф состояний ────────────────────────────────────────────────
|
||||
entity: wallet::withdraw
|
||||
@@ -80,8 +82,8 @@ states:
|
||||
- name: pending
|
||||
human: Ожидает решения совета
|
||||
description: >
|
||||
Заявка создана, сумма заблокирована на кошельке пайщика (available → blocked),
|
||||
повестка отправлена в совет.
|
||||
Заявка создана, сумма переведена в кошелёк-резерв (SHARE_FUND_PAY →
|
||||
WITHDRAW_PENDING), повестка отправлена в совет.
|
||||
kind: normal
|
||||
- name: authorized
|
||||
human: Выплата отправлена
|
||||
@@ -92,15 +94,15 @@ states:
|
||||
- name: completed
|
||||
human: Возврат выплачен
|
||||
description: >
|
||||
Gateway подтвердил списание, заблокированная сумма сжигается на кошельке
|
||||
пайщика (BURN_BLOCKED w.wal.share). В бухгалтерии прошла обратная
|
||||
Gateway подтвердил списание, сумма из резерва сжигается
|
||||
(BURN w.wal.wpend). В бухгалтерии прошла обратная
|
||||
проводка Дт 80 / Кт 51. Запись заявки удалена.
|
||||
kind: final
|
||||
- name: removed
|
||||
human: Отклонено
|
||||
description: >
|
||||
Возврат не состоялся: сумма разблокирована (UNBLOCK через o.wal.wthdec),
|
||||
запись удалена.
|
||||
Возврат не состоялся: сумма возвращена из резерва на кошелёк пайщика
|
||||
(o.wal.wthdec: WITHDRAW_PENDING → SHARE_FUND_PAY), запись удалена.
|
||||
kind: virtual
|
||||
virtual: true
|
||||
|
||||
@@ -172,7 +174,7 @@ scenario:
|
||||
- Сумма возврата ≤ доступного остатка.
|
||||
post:
|
||||
- В таблице withdraws создана запись со статусом `pending`.
|
||||
- На кошельке пайщика сумма переведена available → blocked.
|
||||
- Сумма переведена с SHARE_FUND_PAY в кошелёк-резерв WITHDRAW_PENDING.
|
||||
- Совет получил повестку о возврате.
|
||||
|
||||
- step: 2
|
||||
@@ -197,13 +199,13 @@ scenario:
|
||||
action: wallet::completewthd
|
||||
description: >
|
||||
Gateway подтверждает списание средств в пользу пайщика. Контракт
|
||||
применяет ledger2-операцию o.wal.wthcpl (BURN_BLOCKED w.wal.share с
|
||||
применяет ledger2-операцию o.wal.wthcpl (BURN w.wal.wpend с
|
||||
обратной проводкой Дт 80 / Кт 51) и удаляет запись заявки.
|
||||
pre:
|
||||
- Заявка в статусе `authorized`.
|
||||
- Gateway подтвердил списание.
|
||||
post:
|
||||
- Заблокированная сумма сожжена с SHARE_FUND_PAY (w.wal.share).
|
||||
- Сумма из резерва сожжена (WITHDRAW_PENDING, w.wal.wpend).
|
||||
- В ledger2 применена операция o.wal.wthcpl.
|
||||
- Запись заявки удалена.
|
||||
|
||||
@@ -214,8 +216,8 @@ scenario:
|
||||
actor: soviet
|
||||
description: >
|
||||
Совет принял отрицательное решение либо Gateway отклонил платёж.
|
||||
Сумма разблокирована на кошельке пайщика (UNBLOCK через o.wal.wthdec),
|
||||
запись удалена.
|
||||
Сумма возвращена из резерва на кошелёк пайщика (o.wal.wthdec:
|
||||
WITHDRAW_PENDING → SHARE_FUND_PAY), запись удалена.
|
||||
|
||||
# ── Секция 5. Документы и подписи ───────────────────────────────────────────
|
||||
documents:
|
||||
@@ -234,45 +236,45 @@ documents:
|
||||
# ── Секция 6. Операции ──────────────────────────────────────────────────────
|
||||
operations:
|
||||
- ledger_code: o.wal.wthreq
|
||||
human_name: Блокировка паевого под запрос на возврат
|
||||
wallet_op: BLOCK
|
||||
human_name: Резервирование паевого под запрос на возврат
|
||||
wallet_op: TRANSFER
|
||||
wallet_from: w.wal.share
|
||||
wallet_to: ~
|
||||
wallet_to: w.wal.wpend
|
||||
debit: ~
|
||||
credit: ~
|
||||
amount_ref: withdraw.quantity
|
||||
triggered_by: wallet::createwthd
|
||||
description: >
|
||||
На создании заявки контракт блокирует сумму на кошельке пайщика
|
||||
(SHARE_FUND_PAY): available → blocked. Бухгалтерская проводка не
|
||||
создаётся — движение внутри одного бухсчёта 80.
|
||||
На создании заявки контракт переводит сумму с кошелька пайщика
|
||||
(SHARE_FUND_PAY) в кошелёк-резерв (WITHDRAW_PENDING). Бухгалтерская
|
||||
проводка не создаётся — движение внутри одного бухсчёта 80.
|
||||
|
||||
- ledger_code: o.wal.wthdec
|
||||
human_name: Разблокировка паевого после отклонения запроса на возврат
|
||||
wallet_op: UNBLOCK
|
||||
wallet_from: w.wal.share
|
||||
wallet_to: ~
|
||||
human_name: Снятие резерва паевого после отклонения запроса на возврат
|
||||
wallet_op: TRANSFER
|
||||
wallet_from: w.wal.wpend
|
||||
wallet_to: w.wal.share
|
||||
debit: ~
|
||||
credit: ~
|
||||
amount_ref: withdraw.quantity
|
||||
triggered_by: wallet::declinewthd
|
||||
description: >
|
||||
Зеркало REQUEST_WITHDRAW: blocked → available на кошельке пайщика.
|
||||
Бухгалтерская проводка не создаётся.
|
||||
Зеркало REQUEST_WITHDRAW: сумма возвращается из резерва (WITHDRAW_PENDING)
|
||||
на кошелёк пайщика (SHARE_FUND_PAY). Бухгалтерская проводка не создаётся.
|
||||
|
||||
- ledger_code: o.wal.wthcpl
|
||||
human_name: Возврат паевого взноса пайщику
|
||||
wallet_op: BURN_BLOCKED
|
||||
wallet_from: w.wal.share # ЦПП «Цифровой Кошелёк» — паевые взносы деньгами
|
||||
wallet_op: BURN
|
||||
wallet_from: w.wal.wpend # кошелёк-резерв возвратов
|
||||
wallet_to: ~ # деньги уходят из системы, получателя на цепи нет
|
||||
debit: 80 # Паевой фонд (складочный капитал)
|
||||
credit: 51 # Расчётный счёт
|
||||
amount_ref: withdraw.quantity
|
||||
triggered_by: wallet::completewthd
|
||||
description: >
|
||||
Обратная операция к o.wal.depcpl: заблокированная сумма сжигается с
|
||||
кошелька пайщика «ЦПП Цифровой Кошелёк» (w.wal.share) — деньги уходят
|
||||
из системы банковским переводом пайщику. На цепи получателя нет.
|
||||
Обратная операция к o.wal.depcpl: зарезервированная сумма сжигается с
|
||||
кошелька-резерва (w.wal.wpend) — деньги уходят из системы банковским
|
||||
переводом пайщику. На цепи получателя нет.
|
||||
В бухгалтерии прошла проводка Дт 80 / Кт 51 — паевой фонд уменьшился,
|
||||
деньги ушли с расчётного счёта пайщику.
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@coopenomics/contracts",
|
||||
"version": "2026.5.23-4",
|
||||
"version": "2026.5.25-3",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
{
|
||||
"watch": ["./src"],
|
||||
"ext": "ts"
|
||||
"ext": "ts",
|
||||
"delay": 2000,
|
||||
"signal": "SIGTERM"
|
||||
}
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "create-nodejs-express-app",
|
||||
"version": "2026.5.23-4",
|
||||
"version": "2026.5.25-3",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "create-nodejs-express-app",
|
||||
"version": "2026.5.23-4",
|
||||
"version": "2026.5.25-3",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@a2seven/yoo-checkout": "^1.1.4",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@coopenomics/controller",
|
||||
"version": "2026.5.23-4",
|
||||
"version": "2026.5.25-3",
|
||||
"description": "Бэкенд GraphQL API кооператива на NestJS",
|
||||
"private": true,
|
||||
"bin": "bin/createNodejsApp.js",
|
||||
@@ -193,12 +193,12 @@
|
||||
"@antfu/eslint-config": "^2.24.1",
|
||||
"@graphql-codegen/cli": "^5.0.3",
|
||||
"@graphql-codegen/typescript": "^4.1.1",
|
||||
"@swc-node/register": "^1.11.1",
|
||||
"@swc/core": "^1.15.33",
|
||||
"@graphql-codegen/typescript-apollo-client-helpers": "^3.0.0",
|
||||
"@graphql-codegen/typescript-operations": "^4.3.1",
|
||||
"@nestjs/testing": "^11.1.19",
|
||||
"@redocly/cli": "^1.18.0",
|
||||
"@swc-node/register": "^1.11.1",
|
||||
"@swc/core": "^1.15.33",
|
||||
"@types/express": "^4.17.21",
|
||||
"@types/node": "^20.11.26",
|
||||
"@typescript-eslint/eslint-plugin": "^5.10.0",
|
||||
|
||||
+1002
-169
File diff suppressed because it is too large
Load Diff
@@ -17,7 +17,4 @@ export class Ledger2WalletDTO {
|
||||
|
||||
@Field(() => String, { description: 'Доступный баланс' })
|
||||
available!: string;
|
||||
|
||||
@Field(() => String, { description: 'Заблокированный баланс' })
|
||||
blocked!: string;
|
||||
}
|
||||
|
||||
@@ -28,9 +28,6 @@ export class ProgramWalletDTO {
|
||||
@Field(() => String, { description: 'Доступный баланс (формат: "100.0000 RUB")' })
|
||||
available!: string;
|
||||
|
||||
@Field(() => String, { description: 'Заблокированный баланс (формат: "100.0000 RUB")' })
|
||||
blocked!: string;
|
||||
|
||||
@Field(() => String, { description: 'Паевой взнос (формат: "100.0000 RUB")' })
|
||||
membership_contribution!: string;
|
||||
|
||||
@@ -49,7 +46,6 @@ export class ProgramWalletDTO {
|
||||
dto.agreement_id = entity.agreement_id;
|
||||
dto.username = entity.username;
|
||||
dto.available = entity.available;
|
||||
dto.blocked = entity.blocked;
|
||||
dto.membership_contribution = entity.membership_contribution;
|
||||
dto.blockNum = entity.blockNum;
|
||||
return dto;
|
||||
|
||||
+11
-3
@@ -256,12 +256,20 @@ export class DecisionExpiredNotificationService implements OnModuleInit, OnModul
|
||||
}
|
||||
}
|
||||
|
||||
// Обновляем дату последней проверки (только это поле, чтобы не перезаписать другие параметры конфига)
|
||||
// Обновляем дату последней проверки. ВАЖНО: read-modify-write по СВЕЖЕМУ
|
||||
// config из БД, а не по захваченному при initialize() снимку `plugin`.
|
||||
// Cron-замыкание держит in-memory снимок с момента boot; за время между
|
||||
// boot и тиком онбординг-флаги (`onboarding_*_done/_hash`) и прочие поля
|
||||
// могли быть записаны в БД другими сервисами. Перезапись устаревшего
|
||||
// снимка стёрла бы их (lost update). Поэтому берём актуальный config и
|
||||
// трогаем только lastCheckDate.
|
||||
const fresh = await this.extensionRepository.findByName('chairman');
|
||||
const baseConfig = fresh?.config ?? plugin.config;
|
||||
const updatedConfig = {
|
||||
...plugin.config,
|
||||
...baseConfig,
|
||||
lastCheckDate: new Date().toISOString(),
|
||||
};
|
||||
await this.extensionRepository.update({ ...plugin, config: updatedConfig });
|
||||
await this.extensionRepository.update({ name: 'chairman', config: updatedConfig });
|
||||
|
||||
this.logger.debug('Проверка истекших решений завершена');
|
||||
} catch (error) {
|
||||
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
/**
|
||||
* Matrix: комната секретаря — создаётся вручную председателем/советом из desktop.
|
||||
* Всегда незашифрована (E2EE секретарь транскрибировать не может). Тип (public/private)
|
||||
* выбирается при создании, поэтому isPrivate здесь не фиксируем — задаёт сервис.
|
||||
*/
|
||||
|
||||
import type { MatrixChatRoomPreset } from './matrix-chat-room-preset.types';
|
||||
|
||||
/**
|
||||
* Те же права, что у комнаты проекта Capital: модераторы (50) почти по всем настройкам;
|
||||
* 100 только у Matrix-админа для критичных state.
|
||||
*/
|
||||
function buildPowerLevels(adminUserId: string): Record<string, unknown> {
|
||||
return {
|
||||
users_default: 0,
|
||||
invite: 50,
|
||||
kick: 50,
|
||||
ban: 50,
|
||||
redact: 50,
|
||||
state_default: 50,
|
||||
events_default: 0,
|
||||
users: {
|
||||
[adminUserId]: 100,
|
||||
},
|
||||
events: {
|
||||
'm.room.name': 50,
|
||||
'm.room.topic': 50,
|
||||
'm.room.avatar': 50,
|
||||
'm.room.pinned_events': 50,
|
||||
'm.room.canonical_alias': 50,
|
||||
'm.room.aliases': 50,
|
||||
'm.room.power_levels': 100,
|
||||
'm.room.history_visibility': 50,
|
||||
'm.room.encryption': 100,
|
||||
'm.room.tombstone': 100,
|
||||
'm.room.server_acl': 100,
|
||||
|
||||
'im.vector.modular.widgets': 50,
|
||||
'm.widget': 50,
|
||||
'org.matrix.msc2876.widgets': 50,
|
||||
'io.element.widgets.layout': 50,
|
||||
|
||||
'io.element.call': 50,
|
||||
'io.element.call.member': 0,
|
||||
'org.matrix.msc3401.call': 50,
|
||||
'org.matrix.msc3401.call.member': 0,
|
||||
'm.call': 50,
|
||||
'm.call.invite': 50,
|
||||
'm.call.answer': 0,
|
||||
'm.call.hangup': 0,
|
||||
'm.call.candidates': 0,
|
||||
'm.call.select_answer': 0,
|
||||
'm.call.reject': 0,
|
||||
'm.call.negotiate': 0,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Базовый пресет комнаты секретаря. `isPrivate` переопределяется сервисом по флагу публичности.
|
||||
* `encrypt` всегда false — иначе секретарь не сможет читать сообщения и транскрибировать.
|
||||
*/
|
||||
export const SECRETARY_ROOM_MATRIX: MatrixChatRoomPreset = {
|
||||
label: 'Комната секретаря',
|
||||
isPrivate: true,
|
||||
encrypt: false,
|
||||
roomType: undefined,
|
||||
initialState: [],
|
||||
buildPowerLevels,
|
||||
};
|
||||
+26
@@ -23,6 +23,32 @@ export class ChatcoopProjectCommunicationRoomDTO {
|
||||
displayLabel!: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Тип непроектной комнаты: пайщики, совет или комната секретаря.
|
||||
*/
|
||||
export enum NonProjectRoomKindGql {
|
||||
MEMBERS = 'MEMBERS',
|
||||
COUNCIL = 'COUNCIL',
|
||||
SECRETARY = 'SECRETARY',
|
||||
}
|
||||
|
||||
registerEnumType(NonProjectRoomKindGql, {
|
||||
name: 'NonProjectRoomKind',
|
||||
description: 'Тип комнаты вне проекта: пайщики, совет, комната секретаря',
|
||||
});
|
||||
|
||||
@ObjectType('ChatcoopNonProjectCommunicationRoom')
|
||||
export class ChatcoopNonProjectCommunicationRoomDTO {
|
||||
@Field({ description: 'Идентификатор комнаты Matrix' })
|
||||
matrixRoomId!: string;
|
||||
|
||||
@Field({ description: 'Подпись для отображения комнаты' })
|
||||
displayLabel!: string;
|
||||
|
||||
@Field(() => NonProjectRoomKindGql, { description: 'Тип комнаты (пайщики / совет / секретарь)' })
|
||||
kind!: NonProjectRoomKindGql;
|
||||
}
|
||||
|
||||
@ObjectType('ChatcoopRoomMessageLine')
|
||||
export class ChatcoopRoomMessageLineDTO {
|
||||
@Field(() => Float, { description: 'origin_server_ts из Matrix (мс)' })
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
import { Field, InputType, ObjectType, registerEnumType } from '@nestjs/graphql';
|
||||
import { IsBoolean, IsNotEmpty, IsString, MaxLength } from 'class-validator';
|
||||
|
||||
/**
|
||||
* Тип комнаты в реестре ChatCoop. Редактировать (удалять) можно только `SECRETARY`;
|
||||
* системные (`MEMBERS`/`COUNCIL`) и проектные (`CAPITAL_PROJECT`) защищены.
|
||||
*/
|
||||
export enum ManagedRoomKindGql {
|
||||
MEMBERS = 'MEMBERS',
|
||||
COUNCIL = 'COUNCIL',
|
||||
CAPITAL_PROJECT = 'CAPITAL_PROJECT',
|
||||
SECRETARY = 'SECRETARY',
|
||||
}
|
||||
|
||||
registerEnumType(ManagedRoomKindGql, {
|
||||
name: 'ManagedRoomKind',
|
||||
description: 'Тип комнаты: пайщики, совет, проект Capital, комната секретаря',
|
||||
});
|
||||
|
||||
@ObjectType('ChatcoopSecretaryRoom')
|
||||
export class ChatcoopSecretaryRoomDTO {
|
||||
@Field({ description: 'Идентификатор комнаты Matrix' })
|
||||
matrixRoomId!: string;
|
||||
|
||||
@Field({ description: 'Название комнаты' })
|
||||
displayLabel!: string;
|
||||
|
||||
@Field(() => ManagedRoomKindGql, { description: 'Тип комнаты' })
|
||||
kind!: ManagedRoomKindGql;
|
||||
|
||||
@Field({ description: 'Комната зашифрована (E2EE) — секретарь не транскрибирует такие комнаты' })
|
||||
encrypted!: boolean;
|
||||
|
||||
@Field({ description: 'Секретарь присутствует в комнате' })
|
||||
secretaryInRoom!: boolean;
|
||||
|
||||
@Field({ description: 'Можно ли удалить комнату из интерфейса (только комнаты секретаря)' })
|
||||
editable!: boolean;
|
||||
}
|
||||
|
||||
@InputType('CreateSecretaryRoomInput')
|
||||
export class CreateSecretaryRoomInputDTO {
|
||||
@Field({ description: 'Название комнаты' })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@MaxLength(240)
|
||||
displayName!: string;
|
||||
|
||||
@Field({ description: 'Публичная комната (любой может войти) либо приватная (вход по приглашению создателя)' })
|
||||
@IsBoolean()
|
||||
isPublic!: boolean;
|
||||
}
|
||||
|
||||
@InputType('RemoveSecretaryRoomInput')
|
||||
export class RemoveSecretaryRoomInputDTO {
|
||||
@Field({ description: 'Идентификатор комнаты Matrix, которую нужно удалить' })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
matrixRoomId!: string;
|
||||
}
|
||||
+32
@@ -11,19 +11,33 @@ import { AuthRoles } from '~/application/auth/decorators/auth.decorator';
|
||||
import { CurrentUser } from '~/application/auth/decorators/current-user.decorator';
|
||||
import type { MonoAccountDomainInterface } from '~/domain/account/interfaces/mono-account-domain.interface';
|
||||
import {
|
||||
ChatcoopNonProjectCommunicationRoomDTO,
|
||||
ChatcoopProjectCommunicationRoomDTO,
|
||||
ChatcoopRoomMessageLineDTO,
|
||||
GetMaxOriginServerTsForRoomInputDTO,
|
||||
GetProjectCommunicationRoomsInputDTO,
|
||||
GetRoomMessagesForUtcDateInputDTO,
|
||||
ListUtcDatesWithNewRoomMessagesInputDTO,
|
||||
NonProjectRoomKindGql,
|
||||
RoomMessageKindGql,
|
||||
} from '../dto/project-communication.dto';
|
||||
import type { InterNonProjectRoomKind } from '@coopenomics/inter';
|
||||
|
||||
function mapKind(kind: 'text' | 'audio'): RoomMessageKindGql {
|
||||
return kind === 'text' ? RoomMessageKindGql.TEXT : RoomMessageKindGql.AUDIO;
|
||||
}
|
||||
|
||||
function mapNonProjectKind(kind: InterNonProjectRoomKind): NonProjectRoomKindGql {
|
||||
switch (kind) {
|
||||
case 'members':
|
||||
return NonProjectRoomKindGql.MEMBERS;
|
||||
case 'council':
|
||||
return NonProjectRoomKindGql.COUNCIL;
|
||||
case 'secretary':
|
||||
return NonProjectRoomKindGql.SECRETARY;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Доступ к данным переписки Capital↔Matrix для синхронизации (blago-cli, секретарь).
|
||||
* Источник — порт `INTER_PROJECT_COMMUNICATION_ARTIFACTS` (пакет inter).
|
||||
@@ -61,6 +75,24 @@ export class ProjectCommunicationResolver {
|
||||
}));
|
||||
}
|
||||
|
||||
@Query(() => [ChatcoopNonProjectCommunicationRoomDTO], {
|
||||
name: 'chatcoopListNonProjectCommunicationRooms',
|
||||
description: 'Комнаты Matrix вне проектов Capital (пайщики/совет/секретарь) — для синхронизации в blago',
|
||||
})
|
||||
@AuthRoles(['chairman', 'member', 'user'])
|
||||
async listNonProjectCommunicationRooms(
|
||||
@CurrentUser() user: MonoAccountDomainInterface
|
||||
): Promise<ChatcoopNonProjectCommunicationRoomDTO[]> {
|
||||
this.ensureComm();
|
||||
this.logger.debug(`chatcoopListNonProjectCommunicationRooms user=${user.username}`);
|
||||
const rooms = await this.comm!.listNonProjectCommunicationRooms();
|
||||
return rooms.map((r) => ({
|
||||
matrixRoomId: r.matrixRoomId,
|
||||
displayLabel: r.displayLabel,
|
||||
kind: mapNonProjectKind(r.kind),
|
||||
}));
|
||||
}
|
||||
|
||||
@Query(() => [String], {
|
||||
name: 'chatcoopListUtcDatesWithNewRoomMessages',
|
||||
description:
|
||||
|
||||
+98
@@ -0,0 +1,98 @@
|
||||
import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
|
||||
import { Logger, UseGuards } from '@nestjs/common';
|
||||
import { ActiveUserStatusGuard } from '~/application/auth/guards/active-user-status.guard';
|
||||
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 { CurrentUser } from '~/application/auth/decorators/current-user.decorator';
|
||||
import type { MonoAccountDomainInterface } from '~/domain/account/interfaces/mono-account-domain.interface';
|
||||
import { SecretaryRoomManagementService } from '../services/secretary-room-management.service';
|
||||
import {
|
||||
ChatcoopSecretaryRoomDTO,
|
||||
CreateSecretaryRoomInputDTO,
|
||||
ManagedRoomKindGql,
|
||||
RemoveSecretaryRoomInputDTO,
|
||||
} from '../dto/secretary-room.dto';
|
||||
import type { ChatcoopManagedMatrixRoomKind } from '../../domain/entities/managed-matrix-room.entity';
|
||||
import type { ManagedMatrixRoomDomainEntity } from '../../domain/entities/managed-matrix-room.entity';
|
||||
|
||||
function mapManagedKind(kind: ChatcoopManagedMatrixRoomKind): ManagedRoomKindGql {
|
||||
switch (kind) {
|
||||
case 'members':
|
||||
return ManagedRoomKindGql.MEMBERS;
|
||||
case 'council':
|
||||
return ManagedRoomKindGql.COUNCIL;
|
||||
case 'capital_project':
|
||||
return ManagedRoomKindGql.CAPITAL_PROJECT;
|
||||
case 'secretary':
|
||||
return ManagedRoomKindGql.SECRETARY;
|
||||
}
|
||||
}
|
||||
|
||||
function toDto(room: ManagedMatrixRoomDomainEntity): ChatcoopSecretaryRoomDTO {
|
||||
return {
|
||||
matrixRoomId: room.matrixRoomId,
|
||||
displayLabel: room.displayLabel || room.matrixRoomId,
|
||||
kind: mapManagedKind(room.kind),
|
||||
encrypted: room.encrypted,
|
||||
secretaryInRoom: room.secretaryInRoom,
|
||||
editable: room.kind === 'secretary',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Управление комнатами секретаря (стол связи desktop).
|
||||
* Доступ — председатель и члены совета: они создают комнаты для звонков с секретарём и удаляют свои.
|
||||
*/
|
||||
@Resolver()
|
||||
@UseGuards(GqlJwtAuthGuard, RolesGuard, ActiveUserStatusGuard)
|
||||
export class SecretaryRoomsResolver {
|
||||
private readonly logger = new Logger(SecretaryRoomsResolver.name);
|
||||
|
||||
constructor(private readonly service: SecretaryRoomManagementService) {}
|
||||
|
||||
@Query(() => [ChatcoopSecretaryRoomDTO], {
|
||||
name: 'chatcoopListSecretaryRooms',
|
||||
description: 'Все комнаты реестра ChatCoop (системные/проектные — read-only, комнаты секретаря — удаляемые)',
|
||||
})
|
||||
@AuthRoles(['chairman', 'member'])
|
||||
async listSecretaryRooms(
|
||||
@CurrentUser() user: MonoAccountDomainInterface
|
||||
): Promise<ChatcoopSecretaryRoomDTO[]> {
|
||||
this.logger.debug(`chatcoopListSecretaryRooms user=${user.username}`);
|
||||
const rooms = await this.service.listRooms();
|
||||
return rooms.map(toDto);
|
||||
}
|
||||
|
||||
@Mutation(() => ChatcoopSecretaryRoomDTO, {
|
||||
name: 'chatcoopCreateSecretaryRoom',
|
||||
description: 'Создать комнату с секретарём (публичную или приватную); секретарь подключается сразу',
|
||||
})
|
||||
@AuthRoles(['chairman', 'member'])
|
||||
async createSecretaryRoom(
|
||||
@CurrentUser() user: MonoAccountDomainInterface,
|
||||
@Args('data', { type: () => CreateSecretaryRoomInputDTO }) data: CreateSecretaryRoomInputDTO
|
||||
): Promise<ChatcoopSecretaryRoomDTO> {
|
||||
this.logger.log(`chatcoopCreateSecretaryRoom user=${user.username} isPublic=${data.isPublic}`);
|
||||
const room = await this.service.createSecretaryRoom({
|
||||
creatorUsername: user.username,
|
||||
creatorRole: user.role,
|
||||
displayName: data.displayName,
|
||||
isPublic: data.isPublic,
|
||||
});
|
||||
return toDto(room);
|
||||
}
|
||||
|
||||
@Mutation(() => String, {
|
||||
name: 'chatcoopRemoveSecretaryRoom',
|
||||
description: 'Удалить комнату секретаря: вывести секретаря и снять комнату с синхронизации (возвращает matrixRoomId)',
|
||||
})
|
||||
@AuthRoles(['chairman', 'member'])
|
||||
async removeSecretaryRoom(
|
||||
@CurrentUser() user: MonoAccountDomainInterface,
|
||||
@Args('data', { type: () => RemoveSecretaryRoomInputDTO }) data: RemoveSecretaryRoomInputDTO
|
||||
): Promise<string> {
|
||||
this.logger.log(`chatcoopRemoveSecretaryRoom user=${user.username} room=${data.matrixRoomId}`);
|
||||
return this.service.removeSecretaryRoom(data.matrixRoomId);
|
||||
}
|
||||
}
|
||||
+42
@@ -818,6 +818,48 @@ export class MatrixApiService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Приглашает пользователя в комнату (от имени Matrix-админа, который состоит в комнате).
|
||||
* Пользователь увидит приглашение и войдёт сам — в отличие от {@link joinRoom} (force-join).
|
||||
*/
|
||||
async inviteUser(userId: string, roomId: string): Promise<void> {
|
||||
try {
|
||||
const adminToken = await this.loginAdmin();
|
||||
await this.httpClient.post(
|
||||
`/_matrix/client/v3/rooms/${encodeURIComponent(roomId)}/invite`,
|
||||
{ user_id: userId },
|
||||
{ headers: { Authorization: `Bearer ${adminToken}` } }
|
||||
);
|
||||
this.logger.log(`Пользователь ${userId} приглашён в комнату ${roomId}`);
|
||||
} catch (error: any) {
|
||||
this.logger.error(
|
||||
`Не удалось пригласить пользователя ${userId} в комнату ${roomId}: ${JSON.stringify(error?.response?.data)}`
|
||||
);
|
||||
throw new Error('Не удалось пригласить пользователя в комнату');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Исключает пользователя из комнаты (от имени Matrix-админа с power 100).
|
||||
* Используется для вывода секретаря из комнаты по требованию председателя/совета.
|
||||
*/
|
||||
async kickUser(userId: string, roomId: string, reason?: string): Promise<void> {
|
||||
try {
|
||||
const adminToken = await this.loginAdmin();
|
||||
await this.httpClient.post(
|
||||
`/_matrix/client/v3/rooms/${encodeURIComponent(roomId)}/kick`,
|
||||
reason ? { user_id: userId, reason } : { user_id: userId },
|
||||
{ headers: { Authorization: `Bearer ${adminToken}` } }
|
||||
);
|
||||
this.logger.log(`Пользователь ${userId} исключён из комнаты ${roomId}`);
|
||||
} catch (error: any) {
|
||||
this.logger.error(
|
||||
`Не удалось исключить пользователя ${userId} из комнаты ${roomId}: ${JSON.stringify(error?.response?.data)}`
|
||||
);
|
||||
throw new Error('Не удалось исключить пользователя из комнаты');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Получает текущие права пользователей в комнате
|
||||
*/
|
||||
|
||||
+148
@@ -0,0 +1,148 @@
|
||||
import { BadRequestException, Inject, Injectable, Logger, NotFoundException } from '@nestjs/common';
|
||||
import { MatrixApiService } from './matrix-api.service';
|
||||
import { ChatCoopApplicationService } from './chatcoop-application.service';
|
||||
import { MatrixUserManagementService } from '../../domain/services/matrix-user-management.service';
|
||||
import { SECRETARY_ROOM_MATRIX } from '../config/matrix-secretary-room.config';
|
||||
import {
|
||||
CHATCOOP_MANAGED_MATRIX_ROOM_REPOSITORY,
|
||||
type ChatcoopManagedMatrixRoomRepository,
|
||||
} from '../../domain/repositories/managed-matrix-room.repository';
|
||||
import {
|
||||
CHATCOOP_STATE_REPOSITORY,
|
||||
type ChatcoopStateRepository,
|
||||
} from '../../domain/repositories/chatcoop-state.repository';
|
||||
import type { ManagedMatrixRoomDomainEntity } from '../../domain/entities/managed-matrix-room.entity';
|
||||
|
||||
export interface CreateSecretaryRoomInput {
|
||||
/** Логин пайщика-создателя (председатель или член совета) */
|
||||
creatorUsername: string;
|
||||
/** Роль создателя в кооперативе — для прав в комнате (chairman / member) */
|
||||
creatorRole: string;
|
||||
/** Название комнаты */
|
||||
displayName: string;
|
||||
/** true — публичная (любой может войти), false — приватная (вход по приглашению) */
|
||||
isPublic: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Управление комнатами секретаря: создание/удаление и список всех комнат реестра.
|
||||
*
|
||||
* Принцип безопасности: секретарь присутствует ТОЛЬКО в комнатах, созданных нашим бэкендом
|
||||
* (kind `secretary`, а также системные members/council и проектные capital_project). Произвольные
|
||||
* «чужие» Matrix-комнаты сюда не подключаются — Synapse общий на все кооперативы, и force-join в
|
||||
* чужую комнату был бы дырой.
|
||||
*/
|
||||
@Injectable()
|
||||
export class SecretaryRoomManagementService {
|
||||
private readonly logger = new Logger(SecretaryRoomManagementService.name);
|
||||
|
||||
constructor(
|
||||
private readonly matrixApi: MatrixApiService,
|
||||
private readonly chatCoopApplicationService: ChatCoopApplicationService,
|
||||
private readonly matrixUserManagement: MatrixUserManagementService,
|
||||
@Inject(CHATCOOP_MANAGED_MATRIX_ROOM_REPOSITORY)
|
||||
private readonly managedRooms: ChatcoopManagedMatrixRoomRepository,
|
||||
@Inject(CHATCOOP_STATE_REPOSITORY)
|
||||
private readonly chatcoopState: ChatcoopStateRepository
|
||||
) {}
|
||||
|
||||
async listRooms(): Promise<ManagedMatrixRoomDomainEntity[]> {
|
||||
return this.managedRooms.findAll();
|
||||
}
|
||||
|
||||
async createSecretaryRoom(input: CreateSecretaryRoomInput): Promise<ManagedMatrixRoomDomainEntity> {
|
||||
const st = await this.chatcoopState.getSingleton();
|
||||
if (!st.isInitialized || !st.spaceId || st.spaceId.trim().length === 0) {
|
||||
throw new BadRequestException('ChatCoop не инициализирован — нельзя создать комнату секретаря');
|
||||
}
|
||||
const secretaryId = st.secretaryMatrixUserId;
|
||||
if (typeof secretaryId !== 'string' || secretaryId.trim().length === 0) {
|
||||
throw new BadRequestException('Секретарь не инициализирован — нельзя создать комнату секретаря');
|
||||
}
|
||||
const displayName = input.displayName.trim();
|
||||
if (displayName.length === 0) {
|
||||
throw new BadRequestException('Название комнаты не может быть пустым');
|
||||
}
|
||||
|
||||
const adminUserId = this.matrixApi.getAdminUserId();
|
||||
const powerLevels = SECRETARY_ROOM_MATRIX.buildPowerLevels(adminUserId);
|
||||
const isPrivate = !input.isPublic;
|
||||
|
||||
const roomId = await this.matrixApi.createRoom(
|
||||
displayName.slice(0, 240),
|
||||
`Комната секретаря — создал ${input.creatorUsername}`,
|
||||
isPrivate,
|
||||
SECRETARY_ROOM_MATRIX.roomType,
|
||||
SECRETARY_ROOM_MATRIX.initialState.length > 0 ? SECRETARY_ROOM_MATRIX.initialState : undefined,
|
||||
// Комната секретаря всегда незашифрована — иначе секретарь не читает сообщения и не транскрибирует.
|
||||
false,
|
||||
powerLevels as Record<string, unknown>
|
||||
);
|
||||
|
||||
await this.matrixApi.addRoomToSpace(st.spaceId.trim(), roomId);
|
||||
|
||||
// Force-join только секретарь.
|
||||
await this.matrixApi.joinRoom(secretaryId.trim(), roomId);
|
||||
|
||||
// Создатель сразу входит в свою комнату с правами модератора.
|
||||
const creatorMatrix = await this.matrixUserManagement.getMatrixUserByCoopUsername(input.creatorUsername);
|
||||
if (creatorMatrix) {
|
||||
try {
|
||||
await this.matrixApi.joinRoom(creatorMatrix.matrixUserId, roomId);
|
||||
await this.chatCoopApplicationService.applyMembersRoomStylePowerForUser(
|
||||
creatorMatrix.matrixUserId,
|
||||
roomId,
|
||||
input.creatorRole
|
||||
);
|
||||
} catch (err) {
|
||||
this.logger.warn(`Не удалось ввести создателя ${input.creatorUsername} в комнату ${roomId}: ${String(err)}`);
|
||||
}
|
||||
} else {
|
||||
this.logger.warn(`Нет Matrix-аккаунта для создателя ${input.creatorUsername} — он войдёт позже сам`);
|
||||
}
|
||||
|
||||
const room = await this.managedRooms.upsertRoom({
|
||||
matrixRoomId: roomId,
|
||||
encrypted: false,
|
||||
kind: 'secretary',
|
||||
displayLabel: displayName,
|
||||
projectHash: null,
|
||||
secretaryInRoom: true,
|
||||
});
|
||||
|
||||
this.logger.log(
|
||||
`Создана комната секретаря ${roomId} (${isPrivate ? 'приватная' : 'публичная'}) создателем ${input.creatorUsername}`
|
||||
);
|
||||
return room;
|
||||
}
|
||||
|
||||
/**
|
||||
* Удаляет комнату секретаря: выводит секретаря из Matrix-комнаты и убирает запись из реестра ChatCoop.
|
||||
* Сама Matrix-комната не уничтожается — её участники сохраняют доступ и историю, но транскрипция/синхронизация прекращаются.
|
||||
* Разрешено только для kind `secretary` — системные и проектные комнаты так удалять нельзя.
|
||||
*/
|
||||
async removeSecretaryRoom(matrixRoomId: string): Promise<string> {
|
||||
const room = await this.managedRooms.findByMatrixRoomId(matrixRoomId);
|
||||
if (!room) {
|
||||
throw new NotFoundException('Комната не найдена в реестре ChatCoop');
|
||||
}
|
||||
if (room.kind !== 'secretary') {
|
||||
throw new BadRequestException('Удалять можно только комнаты секретаря (системные и проектные защищены)');
|
||||
}
|
||||
|
||||
const st = await this.chatcoopState.getSingleton();
|
||||
const secretaryId = st.secretaryMatrixUserId;
|
||||
if (typeof secretaryId === 'string' && secretaryId.trim().length > 0) {
|
||||
try {
|
||||
await this.matrixApi.kickUser(secretaryId.trim(), matrixRoomId, 'Комната секретаря удалена');
|
||||
} catch (err) {
|
||||
this.logger.warn(`Не удалось вывести секретаря из ${matrixRoomId}: ${String(err)}`);
|
||||
}
|
||||
}
|
||||
|
||||
await this.managedRooms.setSecretaryInRoom(matrixRoomId, false);
|
||||
await this.managedRooms.deleteByMatrixRoomId(matrixRoomId);
|
||||
this.logger.log(`Комната секретаря ${matrixRoomId} удалена из реестра`);
|
||||
return matrixRoomId;
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,8 @@ import { ChatCoopCalendarResolver } from './application/resolvers/chatcoop-calen
|
||||
import { ChatCoopCalendarFeedController } from './application/controllers/chatcoop-calendar-feed.controller';
|
||||
import { TranscriptionResolver } from './application/resolvers/transcription.resolver';
|
||||
import { ProjectCommunicationResolver } from './application/resolvers/project-communication.resolver';
|
||||
import { SecretaryRoomsResolver } from './application/resolvers/secretary-rooms.resolver';
|
||||
import { SecretaryRoomManagementService } from './application/services/secretary-room-management.service';
|
||||
import { WinstonLoggerService } from '~/application/logger/logger-app.service';
|
||||
import { ConfigModule } from '@nestjs/config';
|
||||
import { z } from 'zod';
|
||||
@@ -527,6 +529,7 @@ export class ChatCoopPlugin extends BaseExtModule {
|
||||
// Application Services
|
||||
ChatCoopApplicationService,
|
||||
CapitalProjectMatrixSyncService,
|
||||
SecretaryRoomManagementService,
|
||||
MatrixApiService,
|
||||
ChatCoopSecretaryMatrixTokenService,
|
||||
SecretaryAgentService,
|
||||
@@ -604,6 +607,7 @@ export class ChatCoopPlugin extends BaseExtModule {
|
||||
TranscriptionResolver,
|
||||
ActiveUserStatusGuard,
|
||||
ProjectCommunicationResolver,
|
||||
SecretaryRoomsResolver,
|
||||
],
|
||||
exports: [
|
||||
ChatCoopPlugin,
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* Тип комнаты Matrix, зарегистрированной в реестре ChatCoop (централизованное хранение).
|
||||
*/
|
||||
export type ChatcoopManagedMatrixRoomKind = 'members' | 'council' | 'capital_project';
|
||||
export type ChatcoopManagedMatrixRoomKind = 'members' | 'council' | 'capital_project' | 'secretary';
|
||||
|
||||
/**
|
||||
* Запись о Matrix-комнате кооператива: источник правды для LiveKit/секретаря; совет при миграции v3 из legacy-конфига.
|
||||
|
||||
+4
@@ -24,6 +24,10 @@ export interface ChatcoopManagedMatrixRoomRepository {
|
||||
findByKind(kind: ChatcoopManagedMatrixRoomKind): Promise<ManagedMatrixRoomDomainEntity[]>;
|
||||
/** Комнаты проекта Capital (kind capital_project, projectHash задан). */
|
||||
findByProjectHash(projectHash: string): Promise<ManagedMatrixRoomDomainEntity[]>;
|
||||
/** Все комнаты реестра (для интерфейса управления присутствием секретаря). */
|
||||
findAll(): Promise<ManagedMatrixRoomDomainEntity[]>;
|
||||
/** Комнаты, не привязанные к проекту Capital (members/council/secretary) — для синхронизации в blago. */
|
||||
findNonProjectCommunicationRooms(): Promise<ManagedMatrixRoomDomainEntity[]>;
|
||||
/** Комнаты, в которых секретарь может участвовать в звонке и писать plaintext в Matrix */
|
||||
findEligibleForSecretaryTranscription(): Promise<ManagedMatrixRoomDomainEntity[]>;
|
||||
/** Обновить флаг членства секретаря (после успешного join или проверки Matrix) */
|
||||
|
||||
+15
@@ -1,6 +1,8 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import type {
|
||||
InterCompletedCallTranscriptionHead,
|
||||
InterNonProjectCommunicationRoomRef,
|
||||
InterNonProjectRoomKind,
|
||||
InterProjectCommunicationArtifactsPort,
|
||||
InterProjectCommunicationRoomRef,
|
||||
InterRoomMessageLine,
|
||||
@@ -48,6 +50,19 @@ export class ChatcoopInterProjectCommunicationArtifactsAdapter implements InterP
|
||||
}));
|
||||
}
|
||||
|
||||
async listNonProjectCommunicationRooms(): Promise<InterNonProjectCommunicationRoomRef[]> {
|
||||
const rooms = await this.managedRooms.findNonProjectCommunicationRooms();
|
||||
return rooms
|
||||
.filter((r): r is typeof r & { kind: InterNonProjectRoomKind } =>
|
||||
r.kind === 'members' || r.kind === 'council' || r.kind === 'secretary'
|
||||
)
|
||||
.map((r) => ({
|
||||
matrixRoomId: r.matrixRoomId,
|
||||
displayLabel: r.displayLabel || r.matrixRoomId,
|
||||
kind: r.kind,
|
||||
}));
|
||||
}
|
||||
|
||||
async listUtcDatesWithNewMessages(
|
||||
matrixRoomId: string,
|
||||
afterOriginServerTsExclusive: number
|
||||
|
||||
+11
-1
@@ -1,6 +1,6 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { Not, Repository } from 'typeorm';
|
||||
import type {
|
||||
ChatcoopManagedMatrixRoomRepository,
|
||||
UpsertManagedMatrixRoomInput,
|
||||
@@ -56,6 +56,16 @@ export class ManagedMatrixRoomTypeormRepository implements ChatcoopManagedMatrix
|
||||
return rows.map(ManagedMatrixRoomMapper.toDomain);
|
||||
}
|
||||
|
||||
async findAll(): Promise<ManagedMatrixRoomDomainEntity[]> {
|
||||
const rows = await this.repository.find();
|
||||
return rows.map(ManagedMatrixRoomMapper.toDomain);
|
||||
}
|
||||
|
||||
async findNonProjectCommunicationRooms(): Promise<ManagedMatrixRoomDomainEntity[]> {
|
||||
const rows = await this.repository.find({ where: { roomKind: Not('capital_project') } });
|
||||
return rows.map(ManagedMatrixRoomMapper.toDomain);
|
||||
}
|
||||
|
||||
async findEligibleForSecretaryTranscription(): Promise<ManagedMatrixRoomDomainEntity[]> {
|
||||
const rows = await this.repository.find({ where: { encrypted: false } });
|
||||
return rows.map(ManagedMatrixRoomMapper.toDomain);
|
||||
|
||||
@@ -64,6 +64,29 @@ export class MeetTrackerService {
|
||||
this.logger.info('MeetTrackerService инициализирован');
|
||||
}
|
||||
|
||||
/**
|
||||
* Сохраняет состояние трекинга собраний read-modify-write по СВЕЖЕМУ config.
|
||||
*
|
||||
* `this.pluginConfig` — снимок, захваченный при initialize() (boot), а
|
||||
* cron-проверка крутится часами; update() заменяет весь config JSONB целиком.
|
||||
* Если писать захваченный снимок, затрутся поля, записанные в БД после boot
|
||||
* другими сервисами (онбординг-флаги и т.п.) — это и есть наблюдавшийся
|
||||
* сброс онбординга. Поэтому берём актуальный config из БД и накладываем
|
||||
* только поля, которыми владеет meet-tracker.
|
||||
*/
|
||||
private async persistTrackedState(): Promise<void> {
|
||||
const fresh = await this.extensionRepository.findByName(this.extensionName);
|
||||
const baseConfig = fresh?.config ?? this.pluginConfig.config;
|
||||
const nextConfig: IConfig = {
|
||||
...baseConfig,
|
||||
trackedMeets: this.pluginConfig.config.trackedMeets,
|
||||
closedMeetIds: this.pluginConfig.config.closedMeetIds,
|
||||
lastCheckTimestamp: this.pluginConfig.config.lastCheckTimestamp,
|
||||
};
|
||||
await this.extensionRepository.update({ name: this.extensionName, config: nextConfig });
|
||||
this.pluginConfig = { ...this.pluginConfig, config: nextConfig };
|
||||
}
|
||||
|
||||
// Приватная функция для обновления trackedMeet на основе свежих данных
|
||||
private getUpdatedTrackedMeet(trackedMeet: TrackedMeet, meetData: any, extendedStatus: string): TrackedMeet {
|
||||
return {
|
||||
@@ -145,7 +168,7 @@ export class MeetTrackerService {
|
||||
this.pluginConfig.config.trackedMeets[trackedMeetIndex].notifications.endNotification = true;
|
||||
if (!closedMeetIds.includes(meetID)) {
|
||||
closedMeetIds.push(meetID);
|
||||
await this.extensionRepository.update(this.pluginConfig);
|
||||
await this.persistTrackedState();
|
||||
}
|
||||
}
|
||||
this.logger.info(`Удаление закрытого собрания ${meetHash} (№${meetID}) из списка отслеживаемых`);
|
||||
@@ -310,7 +333,7 @@ export class MeetTrackerService {
|
||||
this.pluginConfig.config.lastCheckTimestamp = now.toISOString();
|
||||
|
||||
// Сохраняем изменения в конфигурации
|
||||
await this.extensionRepository.update(this.pluginConfig);
|
||||
await this.persistTrackedState();
|
||||
} catch (error: any) {
|
||||
this.logger.error(`Ошибка при проверке собраний: ${error.message}`, error.stack);
|
||||
}
|
||||
|
||||
@@ -215,8 +215,18 @@ export class PowerupPlugin extends BaseExtModule implements OnModuleDestroy {
|
||||
|
||||
await this.blockchainPort.powerUp(username, quantity);
|
||||
|
||||
this.plugin.config.lastDailyReplenishmentDate = new Date().toISOString();
|
||||
await this.extensionRepository.update(this.plugin);
|
||||
// read-modify-write по СВЕЖЕМУ config: daily-cron держит in-memory снимок
|
||||
// `this.plugin` с момента boot, а update() заменяет весь config JSONB
|
||||
// целиком. Перезапись устаревшего снимка стёрла бы поля, записанные за
|
||||
// сутки другими сервисами (онбординг и т.п.). Берём актуальный config и
|
||||
// трогаем только lastDailyReplenishmentDate.
|
||||
const fresh = await this.extensionRepository.findByName(this.name);
|
||||
const nextConfig = {
|
||||
...(fresh?.config ?? this.plugin.config),
|
||||
lastDailyReplenishmentDate: new Date().toISOString(),
|
||||
};
|
||||
await this.extensionRepository.update({ name: this.name, config: nextConfig });
|
||||
this.plugin = { ...this.plugin, config: nextConfig };
|
||||
|
||||
await this.log({
|
||||
type: 'daily',
|
||||
|
||||
@@ -341,6 +341,9 @@ export const AllTypesProps: Record<string,any> = {
|
||||
},
|
||||
CreateProjectPropertyInput:{
|
||||
|
||||
},
|
||||
CreateSecretaryRoomInput:{
|
||||
|
||||
},
|
||||
CreateSovietIndividualDataInput:{
|
||||
passport:"PassportInput"
|
||||
@@ -617,6 +620,7 @@ export const AllTypesProps: Record<string,any> = {
|
||||
MakeClearanceInput:{
|
||||
document:"SignedDigitalDocumentInput"
|
||||
},
|
||||
ManagedRoomKind: "enum" as const,
|
||||
MarkReportPeriodInput:{
|
||||
mark:"ReportSubmissionMark",
|
||||
reportType:"ReportType"
|
||||
@@ -899,8 +903,14 @@ export const AllTypesProps: Record<string,any> = {
|
||||
chatcoopCreateCalendarEvent:{
|
||||
data:"CreateChatCoopCalendarEventInput"
|
||||
},
|
||||
chatcoopCreateSecretaryRoom:{
|
||||
data:"CreateSecretaryRoomInput"
|
||||
},
|
||||
chatcoopDeleteCalendarEvent:{
|
||||
|
||||
},
|
||||
chatcoopRemoveSecretaryRoom:{
|
||||
data:"RemoveSecretaryRoomInput"
|
||||
},
|
||||
chatcoopUpdateCalendarEvent:{
|
||||
data:"UpdateChatCoopCalendarEventInput"
|
||||
@@ -1214,6 +1224,7 @@ export const AllTypesProps: Record<string,any> = {
|
||||
input:"WalmoveInput"
|
||||
}
|
||||
},
|
||||
NonProjectRoomKind: "enum" as const,
|
||||
NotificationWorkflowRecipientInput:{
|
||||
|
||||
},
|
||||
@@ -1643,6 +1654,9 @@ export const AllTypesProps: Record<string,any> = {
|
||||
statement:"ParticipantApplicationSignedDocumentInput",
|
||||
user_agreement:"SignedDigitalDocumentInput",
|
||||
wallet_agreement:"SignedDigitalDocumentInput"
|
||||
},
|
||||
RemoveSecretaryRoomInput:{
|
||||
|
||||
},
|
||||
ReportHistoryFilterInput:{
|
||||
reportType:"ReportType"
|
||||
@@ -2920,6 +2934,11 @@ export const ReturnTypes: Record<string,any> = {
|
||||
displayLabel:"String",
|
||||
matrixRoomId:"String"
|
||||
},
|
||||
ChatcoopNonProjectCommunicationRoom:{
|
||||
displayLabel:"String",
|
||||
kind:"NonProjectRoomKind",
|
||||
matrixRoomId:"String"
|
||||
},
|
||||
ChatcoopProjectCommunicationRoom:{
|
||||
displayLabel:"String",
|
||||
matrixRoomId:"String"
|
||||
@@ -2931,6 +2950,14 @@ export const ReturnTypes: Record<string,any> = {
|
||||
kind:"RoomMessageKind",
|
||||
originServerTs:"Float"
|
||||
},
|
||||
ChatcoopSecretaryRoom:{
|
||||
displayLabel:"String",
|
||||
editable:"Boolean",
|
||||
encrypted:"Boolean",
|
||||
kind:"ManagedRoomKind",
|
||||
matrixRoomId:"String",
|
||||
secretaryInRoom:"Boolean"
|
||||
},
|
||||
ContactsDTO:{
|
||||
chairman:"PublicChairman",
|
||||
details:"OrganizationDetails",
|
||||
@@ -3343,7 +3370,6 @@ export const ReturnTypes: Record<string,any> = {
|
||||
},
|
||||
Ledger2Wallet:{
|
||||
available:"String",
|
||||
blocked:"String",
|
||||
id:"String",
|
||||
name:"String"
|
||||
},
|
||||
@@ -3560,7 +3586,9 @@ export const ReturnTypes: Record<string,any> = {
|
||||
chatcoopCreateAccount:"Boolean",
|
||||
chatcoopCreateCalendarEvent:"ChatCoopCalendarEvent",
|
||||
chatcoopCreateCalendarIcsSubscription:"ChatCoopCalendarIcsUrlResponse",
|
||||
chatcoopCreateSecretaryRoom:"ChatcoopSecretaryRoom",
|
||||
chatcoopDeleteCalendarEvent:"Boolean",
|
||||
chatcoopRemoveSecretaryRoom:"String",
|
||||
chatcoopUpdateCalendarEvent:"ChatCoopCalendarEvent",
|
||||
chatcoopUpdateTranscriptionMemo:"CallTranscription",
|
||||
completeCapitalOnboardingStep:"CapitalOnboardingState",
|
||||
@@ -4041,7 +4069,6 @@ export const ReturnTypes: Record<string,any> = {
|
||||
agreement_id:"ID",
|
||||
available:"String",
|
||||
blockNum:"Float",
|
||||
blocked:"String",
|
||||
coopname:"String",
|
||||
id:"ID",
|
||||
membership_contribution:"String",
|
||||
@@ -4130,7 +4157,9 @@ export const ReturnTypes: Record<string,any> = {
|
||||
chatcoopGetTranscriptions:"CallTranscription",
|
||||
chatcoopListCalendarEvents:"ChatCoopCalendarEvent",
|
||||
chatcoopListCalendarRooms:"ChatCoopCalendarRoomOption",
|
||||
chatcoopListNonProjectCommunicationRooms:"ChatcoopNonProjectCommunicationRoom",
|
||||
chatcoopListProjectCommunicationRooms:"ChatcoopProjectCommunicationRoom",
|
||||
chatcoopListSecretaryRooms:"ChatcoopSecretaryRoom",
|
||||
chatcoopListUtcDatesWithNewRoomMessages:"String",
|
||||
checkReportReadiness:"ReportReadinessView",
|
||||
cooperativeAgreements:"CoopAgreement",
|
||||
|
||||
@@ -3851,6 +3851,16 @@ export type ValueTypes = {
|
||||
matrixRoomId?:boolean | `@${string}`,
|
||||
__typename?: boolean | `@${string}`,
|
||||
['...on ChatCoopCalendarRoomOption']?: Omit<ValueTypes["ChatCoopCalendarRoomOption"], "...on ChatCoopCalendarRoomOption">
|
||||
}>;
|
||||
["ChatcoopNonProjectCommunicationRoom"]: AliasType<{
|
||||
/** Подпись для отображения комнаты */
|
||||
displayLabel?:boolean | `@${string}`,
|
||||
/** Тип комнаты (пайщики / совет / секретарь) */
|
||||
kind?:boolean | `@${string}`,
|
||||
/** Идентификатор комнаты Matrix */
|
||||
matrixRoomId?:boolean | `@${string}`,
|
||||
__typename?: boolean | `@${string}`,
|
||||
['...on ChatcoopNonProjectCommunicationRoom']?: Omit<ValueTypes["ChatcoopNonProjectCommunicationRoom"], "...on ChatcoopNonProjectCommunicationRoom">
|
||||
}>;
|
||||
["ChatcoopProjectCommunicationRoom"]: AliasType<{
|
||||
/** Подпись для отображения (комната / проект Capital) */
|
||||
@@ -3872,6 +3882,22 @@ export type ValueTypes = {
|
||||
originServerTs?:boolean | `@${string}`,
|
||||
__typename?: boolean | `@${string}`,
|
||||
['...on ChatcoopRoomMessageLine']?: Omit<ValueTypes["ChatcoopRoomMessageLine"], "...on ChatcoopRoomMessageLine">
|
||||
}>;
|
||||
["ChatcoopSecretaryRoom"]: AliasType<{
|
||||
/** Название комнаты */
|
||||
displayLabel?:boolean | `@${string}`,
|
||||
/** Можно ли удалить комнату из интерфейса (только комнаты секретаря) */
|
||||
editable?:boolean | `@${string}`,
|
||||
/** Комната зашифрована (E2EE) — секретарь не транскрибирует такие комнаты */
|
||||
encrypted?:boolean | `@${string}`,
|
||||
/** Тип комнаты */
|
||||
kind?:boolean | `@${string}`,
|
||||
/** Идентификатор комнаты Matrix */
|
||||
matrixRoomId?:boolean | `@${string}`,
|
||||
/** Секретарь присутствует в комнате */
|
||||
secretaryInRoom?:boolean | `@${string}`,
|
||||
__typename?: boolean | `@${string}`,
|
||||
['...on ChatcoopSecretaryRoom']?: Omit<ValueTypes["ChatcoopSecretaryRoom"], "...on ChatcoopSecretaryRoom">
|
||||
}>;
|
||||
["CheckMatrixUsernameInput"]: {
|
||||
username: string | Variable<any, string>
|
||||
@@ -4604,6 +4630,12 @@ export type ValueTypes = {
|
||||
property_hash: string | Variable<any, string>,
|
||||
/** Имя пользователя */
|
||||
username: string | Variable<any, string>
|
||||
};
|
||||
["CreateSecretaryRoomInput"]: {
|
||||
/** Название комнаты */
|
||||
displayName: string | Variable<any, string>,
|
||||
/** Публичная комната (любой может войти) либо приватная (вход по приглашению создателя) */
|
||||
isPublic: boolean | Variable<any, string>
|
||||
};
|
||||
["CreateSovietIndividualDataInput"]: {
|
||||
/** Дата рождения */
|
||||
@@ -5921,6 +5953,8 @@ export type ValueTypes = {
|
||||
['...on IndividualCertificate']?: Omit<ValueTypes["IndividualCertificate"], "...on IndividualCertificate">
|
||||
}>;
|
||||
["Init"]: {
|
||||
/** Признак того, что инициализация выполняется со стороны провайдера. При true coopback ставит init_by_server=true (org_data становится readonly для пользовательского визарда). Поле передаёт provider в callInitSystemMutation. */
|
||||
is_server_init?: boolean | undefined | null | Variable<any, string>,
|
||||
/** Объект организации кооператива, которая обслуживает данный экземпляр программного обеспечения MONO */
|
||||
organization_data: ValueTypes["CreateInitOrganizationDataInput"] | Variable<any, string>
|
||||
};
|
||||
@@ -6052,8 +6086,6 @@ export type ValueTypes = {
|
||||
["Ledger2Wallet"]: AliasType<{
|
||||
/** Доступный баланс */
|
||||
available?:boolean | `@${string}`,
|
||||
/** Заблокированный баланс */
|
||||
blocked?:boolean | `@${string}`,
|
||||
/** eosio::name-идентификатор кошелька (w.<contract>.<waltype>) */
|
||||
id?:boolean | `@${string}`,
|
||||
/** Название кошелька */
|
||||
@@ -6143,6 +6175,8 @@ export type ValueTypes = {
|
||||
/** Имя пользователя */
|
||||
username: string | Variable<any, string>
|
||||
};
|
||||
/** Тип комнаты: пайщики, совет, проект Capital, комната секретаря */
|
||||
["ManagedRoomKind"]:ManagedRoomKind;
|
||||
["MarkReportPeriodInput"]: {
|
||||
mark?: ValueTypes["ReportSubmissionMark"] | undefined | null | Variable<any, string>,
|
||||
period?: number | undefined | null | Variable<any, string>,
|
||||
@@ -6466,7 +6500,9 @@ chatcoopCreateCalendarEvent?: [{ data: ValueTypes["CreateChatCoopCalendarEventIn
|
||||
|
||||
Требуемые роли: chairman, member, user. */
|
||||
chatcoopCreateCalendarIcsSubscription?:ValueTypes["ChatCoopCalendarIcsUrlResponse"],
|
||||
chatcoopCreateSecretaryRoom?: [{ data: ValueTypes["CreateSecretaryRoomInput"] | Variable<any, string>},ValueTypes["ChatcoopSecretaryRoom"]],
|
||||
chatcoopDeleteCalendarEvent?: [{ id: string | Variable<any, string>},boolean | `@${string}`],
|
||||
chatcoopRemoveSecretaryRoom?: [{ data: ValueTypes["RemoveSecretaryRoomInput"] | Variable<any, string>},boolean | `@${string}`],
|
||||
chatcoopUpdateCalendarEvent?: [{ data: ValueTypes["UpdateChatCoopCalendarEventInput"] | Variable<any, string>},ValueTypes["ChatCoopCalendarEvent"]],
|
||||
chatcoopUpdateTranscriptionMemo?: [{ data: ValueTypes["UpdateCallTranscriptionMemoInput"] | Variable<any, string>},ValueTypes["CallTranscription"]],
|
||||
completeCapitalOnboardingStep?: [{ data: ValueTypes["CapitalOnboardingStepInput"] | Variable<any, string>},ValueTypes["CapitalOnboardingState"]],
|
||||
@@ -6566,6 +6602,8 @@ walmoveWallets?: [{ input: ValueTypes["WalmoveInput"] | Variable<any, string>},V
|
||||
__typename?: boolean | `@${string}`,
|
||||
['...on Mutation']?: Omit<ValueTypes["Mutation"], "...on Mutation">
|
||||
}>;
|
||||
/** Тип комнаты вне проекта: пайщики, совет, комната секретаря */
|
||||
["NonProjectRoomKind"]:NonProjectRoomKind;
|
||||
["NotificationWorkflowRecipientInput"]: {
|
||||
/** Username получателя */
|
||||
username: string | Variable<any, string>
|
||||
@@ -7503,8 +7541,6 @@ walmoveWallets?: [{ input: ValueTypes["WalmoveInput"] | Variable<any, string>},V
|
||||
available?:boolean | `@${string}`,
|
||||
/** Номер блока последнего обновления */
|
||||
blockNum?:boolean | `@${string}`,
|
||||
/** Заблокированный баланс (формат: "100.0000 RUB") */
|
||||
blocked?:boolean | `@${string}`,
|
||||
/** Имя кооператива */
|
||||
coopname?:boolean | `@${string}`,
|
||||
/** Уникальный идентификатор кошелька в блокчейне */
|
||||
@@ -7776,7 +7812,15 @@ chatcoopGetTranscriptions?: [{ data?: ValueTypes["GetTranscriptionsInput"] | und
|
||||
|
||||
Требуемые роли: chairman, member. */
|
||||
chatcoopListCalendarRooms?:ValueTypes["ChatCoopCalendarRoomOption"],
|
||||
/** Комнаты Matrix вне проектов Capital (пайщики/совет/секретарь) — для синхронизации в blago
|
||||
|
||||
Требуемые роли: chairman, member, user. */
|
||||
chatcoopListNonProjectCommunicationRooms?:ValueTypes["ChatcoopNonProjectCommunicationRoom"],
|
||||
chatcoopListProjectCommunicationRooms?: [{ data: ValueTypes["GetProjectCommunicationRoomsInput"] | Variable<any, string>},ValueTypes["ChatcoopProjectCommunicationRoom"]],
|
||||
/** Все комнаты реестра ChatCoop (системные/проектные — read-only, комнаты секретаря — удаляемые)
|
||||
|
||||
Требуемые роли: chairman, member. */
|
||||
chatcoopListSecretaryRooms?:ValueTypes["ChatcoopSecretaryRoom"],
|
||||
chatcoopListUtcDatesWithNewRoomMessages?: [{ data: ValueTypes["ListUtcDatesWithNewRoomMessagesInput"] | Variable<any, string>},boolean | `@${string}`],
|
||||
checkReportReadiness?: [{ reportType: ValueTypes["ReportType"] | Variable<any, string>},ValueTypes["ReportReadinessView"]],
|
||||
cooperativeAgreements?: [{ coopname: string | Variable<any, string>},ValueTypes["CoopAgreement"]],
|
||||
@@ -8049,6 +8093,10 @@ validateReportEdits?: [{ editsJson: string | Variable<any, string>, reportType:
|
||||
__typename?: boolean | `@${string}`,
|
||||
['...on RegistrationProgram']?: Omit<ValueTypes["RegistrationProgram"], "...on RegistrationProgram">
|
||||
}>;
|
||||
["RemoveSecretaryRoomInput"]: {
|
||||
/** Идентификатор комнаты Matrix, которую нужно удалить */
|
||||
matrixRoomId: string | Variable<any, string>
|
||||
};
|
||||
["ReportCalendarPeriodEntry"]: AliasType<{
|
||||
dueDate?:boolean | `@${string}`,
|
||||
dueMonth?:boolean | `@${string}`,
|
||||
@@ -9118,7 +9166,7 @@ validateReportEdits?: [{ editsJson: string | Variable<any, string>, reportType:
|
||||
};
|
||||
["UpdateCallTranscriptionMemoInput"]: {
|
||||
id: string | Variable<any, string>,
|
||||
/** Текст заметки (до 4000 символов) */
|
||||
/** Текст заметки */
|
||||
memo: string | Variable<any, string>
|
||||
};
|
||||
["UpdateChatCoopCalendarEventInput"]: {
|
||||
@@ -12196,6 +12244,15 @@ export type ResolverInputTypes = {
|
||||
displayLabel?:boolean | `@${string}`,
|
||||
matrixRoomId?:boolean | `@${string}`,
|
||||
__typename?: boolean | `@${string}`
|
||||
}>;
|
||||
["ChatcoopNonProjectCommunicationRoom"]: AliasType<{
|
||||
/** Подпись для отображения комнаты */
|
||||
displayLabel?:boolean | `@${string}`,
|
||||
/** Тип комнаты (пайщики / совет / секретарь) */
|
||||
kind?:boolean | `@${string}`,
|
||||
/** Идентификатор комнаты Matrix */
|
||||
matrixRoomId?:boolean | `@${string}`,
|
||||
__typename?: boolean | `@${string}`
|
||||
}>;
|
||||
["ChatcoopProjectCommunicationRoom"]: AliasType<{
|
||||
/** Подпись для отображения (комната / проект Capital) */
|
||||
@@ -12215,6 +12272,21 @@ export type ResolverInputTypes = {
|
||||
/** origin_server_ts из Matrix (мс) */
|
||||
originServerTs?:boolean | `@${string}`,
|
||||
__typename?: boolean | `@${string}`
|
||||
}>;
|
||||
["ChatcoopSecretaryRoom"]: AliasType<{
|
||||
/** Название комнаты */
|
||||
displayLabel?:boolean | `@${string}`,
|
||||
/** Можно ли удалить комнату из интерфейса (только комнаты секретаря) */
|
||||
editable?:boolean | `@${string}`,
|
||||
/** Комната зашифрована (E2EE) — секретарь не транскрибирует такие комнаты */
|
||||
encrypted?:boolean | `@${string}`,
|
||||
/** Тип комнаты */
|
||||
kind?:boolean | `@${string}`,
|
||||
/** Идентификатор комнаты Matrix */
|
||||
matrixRoomId?:boolean | `@${string}`,
|
||||
/** Секретарь присутствует в комнате */
|
||||
secretaryInRoom?:boolean | `@${string}`,
|
||||
__typename?: boolean | `@${string}`
|
||||
}>;
|
||||
["CheckMatrixUsernameInput"]: {
|
||||
username: string
|
||||
@@ -12942,6 +13014,12 @@ export type ResolverInputTypes = {
|
||||
property_hash: string,
|
||||
/** Имя пользователя */
|
||||
username: string
|
||||
};
|
||||
["CreateSecretaryRoomInput"]: {
|
||||
/** Название комнаты */
|
||||
displayName: string,
|
||||
/** Публичная комната (любой может войти) либо приватная (вход по приглашению создателя) */
|
||||
isPublic: boolean
|
||||
};
|
||||
["CreateSovietIndividualDataInput"]: {
|
||||
/** Дата рождения */
|
||||
@@ -14227,6 +14305,8 @@ export type ResolverInputTypes = {
|
||||
__typename?: boolean | `@${string}`
|
||||
}>;
|
||||
["Init"]: {
|
||||
/** Признак того, что инициализация выполняется со стороны провайдера. При true coopback ставит init_by_server=true (org_data становится readonly для пользовательского визарда). Поле передаёт provider в callInitSystemMutation. */
|
||||
is_server_init?: boolean | undefined | null,
|
||||
/** Объект организации кооператива, которая обслуживает данный экземпляр программного обеспечения MONO */
|
||||
organization_data: ResolverInputTypes["CreateInitOrganizationDataInput"]
|
||||
};
|
||||
@@ -14350,8 +14430,6 @@ export type ResolverInputTypes = {
|
||||
["Ledger2Wallet"]: AliasType<{
|
||||
/** Доступный баланс */
|
||||
available?:boolean | `@${string}`,
|
||||
/** Заблокированный баланс */
|
||||
blocked?:boolean | `@${string}`,
|
||||
/** eosio::name-идентификатор кошелька (w.<contract>.<waltype>) */
|
||||
id?:boolean | `@${string}`,
|
||||
/** Название кошелька */
|
||||
@@ -14437,6 +14515,8 @@ export type ResolverInputTypes = {
|
||||
/** Имя пользователя */
|
||||
username: string
|
||||
};
|
||||
/** Тип комнаты: пайщики, совет, проект Capital, комната секретаря */
|
||||
["ManagedRoomKind"]:ManagedRoomKind;
|
||||
["MarkReportPeriodInput"]: {
|
||||
mark?: ResolverInputTypes["ReportSubmissionMark"] | undefined | null,
|
||||
period?: number | undefined | null,
|
||||
@@ -14751,7 +14831,9 @@ chatcoopCreateCalendarEvent?: [{ data: ResolverInputTypes["CreateChatCoopCalenda
|
||||
|
||||
Требуемые роли: chairman, member, user. */
|
||||
chatcoopCreateCalendarIcsSubscription?:ResolverInputTypes["ChatCoopCalendarIcsUrlResponse"],
|
||||
chatcoopCreateSecretaryRoom?: [{ data: ResolverInputTypes["CreateSecretaryRoomInput"]},ResolverInputTypes["ChatcoopSecretaryRoom"]],
|
||||
chatcoopDeleteCalendarEvent?: [{ id: string},boolean | `@${string}`],
|
||||
chatcoopRemoveSecretaryRoom?: [{ data: ResolverInputTypes["RemoveSecretaryRoomInput"]},boolean | `@${string}`],
|
||||
chatcoopUpdateCalendarEvent?: [{ data: ResolverInputTypes["UpdateChatCoopCalendarEventInput"]},ResolverInputTypes["ChatCoopCalendarEvent"]],
|
||||
chatcoopUpdateTranscriptionMemo?: [{ data: ResolverInputTypes["UpdateCallTranscriptionMemoInput"]},ResolverInputTypes["CallTranscription"]],
|
||||
completeCapitalOnboardingStep?: [{ data: ResolverInputTypes["CapitalOnboardingStepInput"]},ResolverInputTypes["CapitalOnboardingState"]],
|
||||
@@ -14850,6 +14932,8 @@ voteOnAnnualGeneralMeet?: [{ data: ResolverInputTypes["VoteOnAnnualGeneralMeetIn
|
||||
walmoveWallets?: [{ input: ResolverInputTypes["WalmoveInput"]},ResolverInputTypes["Ledger2AdjustmentResult"]],
|
||||
__typename?: boolean | `@${string}`
|
||||
}>;
|
||||
/** Тип комнаты вне проекта: пайщики, совет, комната секретаря */
|
||||
["NonProjectRoomKind"]:NonProjectRoomKind;
|
||||
["NotificationWorkflowRecipientInput"]: {
|
||||
/** Username получателя */
|
||||
username: string
|
||||
@@ -15737,8 +15821,6 @@ walmoveWallets?: [{ input: ResolverInputTypes["WalmoveInput"]},ResolverInputType
|
||||
available?:boolean | `@${string}`,
|
||||
/** Номер блока последнего обновления */
|
||||
blockNum?:boolean | `@${string}`,
|
||||
/** Заблокированный баланс (формат: "100.0000 RUB") */
|
||||
blocked?:boolean | `@${string}`,
|
||||
/** Имя кооператива */
|
||||
coopname?:boolean | `@${string}`,
|
||||
/** Уникальный идентификатор кошелька в блокчейне */
|
||||
@@ -16006,7 +16088,15 @@ chatcoopGetTranscriptions?: [{ data?: ResolverInputTypes["GetTranscriptionsInput
|
||||
|
||||
Требуемые роли: chairman, member. */
|
||||
chatcoopListCalendarRooms?:ResolverInputTypes["ChatCoopCalendarRoomOption"],
|
||||
/** Комнаты Matrix вне проектов Capital (пайщики/совет/секретарь) — для синхронизации в blago
|
||||
|
||||
Требуемые роли: chairman, member, user. */
|
||||
chatcoopListNonProjectCommunicationRooms?:ResolverInputTypes["ChatcoopNonProjectCommunicationRoom"],
|
||||
chatcoopListProjectCommunicationRooms?: [{ data: ResolverInputTypes["GetProjectCommunicationRoomsInput"]},ResolverInputTypes["ChatcoopProjectCommunicationRoom"]],
|
||||
/** Все комнаты реестра ChatCoop (системные/проектные — read-only, комнаты секретаря — удаляемые)
|
||||
|
||||
Требуемые роли: chairman, member. */
|
||||
chatcoopListSecretaryRooms?:ResolverInputTypes["ChatcoopSecretaryRoom"],
|
||||
chatcoopListUtcDatesWithNewRoomMessages?: [{ data: ResolverInputTypes["ListUtcDatesWithNewRoomMessagesInput"]},boolean | `@${string}`],
|
||||
checkReportReadiness?: [{ reportType: ResolverInputTypes["ReportType"]},ResolverInputTypes["ReportReadinessView"]],
|
||||
cooperativeAgreements?: [{ coopname: string},ResolverInputTypes["CoopAgreement"]],
|
||||
@@ -16272,6 +16362,10 @@ validateReportEdits?: [{ editsJson: string, reportType: ResolverInputTypes["Repo
|
||||
title?:boolean | `@${string}`,
|
||||
__typename?: boolean | `@${string}`
|
||||
}>;
|
||||
["RemoveSecretaryRoomInput"]: {
|
||||
/** Идентификатор комнаты Matrix, которую нужно удалить */
|
||||
matrixRoomId: string
|
||||
};
|
||||
["ReportCalendarPeriodEntry"]: AliasType<{
|
||||
dueDate?:boolean | `@${string}`,
|
||||
dueMonth?:boolean | `@${string}`,
|
||||
@@ -17311,7 +17405,7 @@ validateReportEdits?: [{ editsJson: string, reportType: ResolverInputTypes["Repo
|
||||
};
|
||||
["UpdateCallTranscriptionMemoInput"]: {
|
||||
id: string,
|
||||
/** Текст заметки (до 4000 символов) */
|
||||
/** Текст заметки */
|
||||
memo: string
|
||||
};
|
||||
["UpdateChatCoopCalendarEventInput"]: {
|
||||
@@ -20307,6 +20401,14 @@ export type ModelTypes = {
|
||||
["ChatCoopCalendarRoomOption"]: {
|
||||
displayLabel: string,
|
||||
matrixRoomId: string
|
||||
};
|
||||
["ChatcoopNonProjectCommunicationRoom"]: {
|
||||
/** Подпись для отображения комнаты */
|
||||
displayLabel: string,
|
||||
/** Тип комнаты (пайщики / совет / секретарь) */
|
||||
kind: ModelTypes["NonProjectRoomKind"],
|
||||
/** Идентификатор комнаты Matrix */
|
||||
matrixRoomId: string
|
||||
};
|
||||
["ChatcoopProjectCommunicationRoom"]: {
|
||||
/** Подпись для отображения (комната / проект Capital) */
|
||||
@@ -20324,6 +20426,20 @@ export type ModelTypes = {
|
||||
kind: ModelTypes["RoomMessageKind"],
|
||||
/** origin_server_ts из Matrix (мс) */
|
||||
originServerTs: number
|
||||
};
|
||||
["ChatcoopSecretaryRoom"]: {
|
||||
/** Название комнаты */
|
||||
displayLabel: string,
|
||||
/** Можно ли удалить комнату из интерфейса (только комнаты секретаря) */
|
||||
editable: boolean,
|
||||
/** Комната зашифрована (E2EE) — секретарь не транскрибирует такие комнаты */
|
||||
encrypted: boolean,
|
||||
/** Тип комнаты */
|
||||
kind: ModelTypes["ManagedRoomKind"],
|
||||
/** Идентификатор комнаты Matrix */
|
||||
matrixRoomId: string,
|
||||
/** Секретарь присутствует в комнате */
|
||||
secretaryInRoom: boolean
|
||||
};
|
||||
["CheckMatrixUsernameInput"]: {
|
||||
username: string
|
||||
@@ -21043,6 +21159,12 @@ export type ModelTypes = {
|
||||
property_hash: string,
|
||||
/** Имя пользователя */
|
||||
username: string
|
||||
};
|
||||
["CreateSecretaryRoomInput"]: {
|
||||
/** Название комнаты */
|
||||
displayName: string,
|
||||
/** Публичная комната (любой может войти) либо приватная (вход по приглашению создателя) */
|
||||
isPublic: boolean
|
||||
};
|
||||
["CreateSovietIndividualDataInput"]: {
|
||||
/** Дата рождения */
|
||||
@@ -22291,6 +22413,8 @@ export type ModelTypes = {
|
||||
username: string
|
||||
};
|
||||
["Init"]: {
|
||||
/** Признак того, что инициализация выполняется со стороны провайдера. При true coopback ставит init_by_server=true (org_data становится readonly для пользовательского визарда). Поле передаёт provider в callInitSystemMutation. */
|
||||
is_server_init?: boolean | undefined | null,
|
||||
/** Объект организации кооператива, которая обслуживает данный экземпляр программного обеспечения MONO */
|
||||
organization_data: ModelTypes["CreateInitOrganizationDataInput"]
|
||||
};
|
||||
@@ -22402,8 +22526,6 @@ export type ModelTypes = {
|
||||
["Ledger2Wallet"]: {
|
||||
/** Доступный баланс */
|
||||
available: string,
|
||||
/** Заблокированный баланс */
|
||||
blocked: string,
|
||||
/** eosio::name-идентификатор кошелька (w.<contract>.<waltype>) */
|
||||
id: string,
|
||||
/** Название кошелька */
|
||||
@@ -22483,6 +22605,7 @@ export type ModelTypes = {
|
||||
/** Имя пользователя */
|
||||
username: string
|
||||
};
|
||||
["ManagedRoomKind"]:ManagedRoomKind;
|
||||
["MarkReportPeriodInput"]: {
|
||||
mark?: ModelTypes["ReportSubmissionMark"] | undefined | null,
|
||||
period?: number | undefined | null,
|
||||
@@ -23027,17 +23150,25 @@ export type ModelTypes = {
|
||||
|
||||
Требуемые роли: chairman, member, user. */
|
||||
chatcoopCreateCalendarIcsSubscription: ModelTypes["ChatCoopCalendarIcsUrlResponse"],
|
||||
/** Создать комнату с секретарём (публичную или приватную); секретарь подключается сразу
|
||||
|
||||
Требуемые роли: chairman, member. */
|
||||
chatcoopCreateSecretaryRoom: ModelTypes["ChatcoopSecretaryRoom"],
|
||||
/** Удалить событие календаря
|
||||
|
||||
Требуемые роли: chairman, member. */
|
||||
chatcoopDeleteCalendarEvent: boolean,
|
||||
/** Удалить комнату секретаря: вывести секретаря и снять комнату с синхронизации (возвращает matrixRoomId)
|
||||
|
||||
Требуемые роли: chairman, member. */
|
||||
chatcoopRemoveSecretaryRoom: string,
|
||||
/** Обновить событие календаря
|
||||
|
||||
Требуемые роли: chairman, member. */
|
||||
chatcoopUpdateCalendarEvent: ModelTypes["ChatCoopCalendarEvent"],
|
||||
/** Обновить заметку (memo) к транскрипции звонка
|
||||
|
||||
Требуемые роли: chairman, member, user. */
|
||||
Требуемые роли: chairman, member. */
|
||||
chatcoopUpdateTranscriptionMemo: ModelTypes["CallTranscription"],
|
||||
/** Выполнить шаг онбординга capital (создание предложения повестки)
|
||||
|
||||
@@ -23356,6 +23487,7 @@ export type ModelTypes = {
|
||||
Требуемые роли: chairman. */
|
||||
walmoveWallets: ModelTypes["Ledger2AdjustmentResult"]
|
||||
};
|
||||
["NonProjectRoomKind"]:NonProjectRoomKind;
|
||||
["NotificationWorkflowRecipientInput"]: {
|
||||
/** Username получателя */
|
||||
username: string
|
||||
@@ -24176,8 +24308,6 @@ export type ModelTypes = {
|
||||
available: string,
|
||||
/** Номер блока последнего обновления */
|
||||
blockNum?: number | undefined | null,
|
||||
/** Заблокированный баланс (формат: "100.0000 RUB") */
|
||||
blocked: string,
|
||||
/** Имя кооператива */
|
||||
coopname: string,
|
||||
/** Уникальный идентификатор кошелька в блокчейне */
|
||||
@@ -24520,10 +24650,18 @@ export type ModelTypes = {
|
||||
|
||||
Требуемые роли: chairman, member. */
|
||||
chatcoopListCalendarRooms: Array<ModelTypes["ChatCoopCalendarRoomOption"]>,
|
||||
/** Комнаты Matrix вне проектов Capital (пайщики/совет/секретарь) — для синхронизации в blago
|
||||
|
||||
Требуемые роли: chairman, member, user. */
|
||||
chatcoopListNonProjectCommunicationRooms: Array<ModelTypes["ChatcoopNonProjectCommunicationRoom"]>,
|
||||
/** Комнаты Matrix, привязанные к проекту Capital (реестр ChatCoop)
|
||||
|
||||
Требуемые роли: chairman, member, user. */
|
||||
chatcoopListProjectCommunicationRooms: Array<ModelTypes["ChatcoopProjectCommunicationRoom"]>,
|
||||
/** Все комнаты реестра ChatCoop (системные/проектные — read-only, комнаты секретаря — удаляемые)
|
||||
|
||||
Требуемые роли: chairman, member. */
|
||||
chatcoopListSecretaryRooms: Array<ModelTypes["ChatcoopSecretaryRoom"]>,
|
||||
/** UTC-даты (YYYY-MM-DD), в которых есть сообщения новее afterOriginServerTsExclusive, для комнаты Matrix
|
||||
|
||||
Требуемые роли: chairman, member, user. */
|
||||
@@ -24896,6 +25034,10 @@ export type ModelTypes = {
|
||||
requirements?: string | undefined | null,
|
||||
/** Название программы для отображения */
|
||||
title: string
|
||||
};
|
||||
["RemoveSecretaryRoomInput"]: {
|
||||
/** Идентификатор комнаты Matrix, которую нужно удалить */
|
||||
matrixRoomId: string
|
||||
};
|
||||
["ReportCalendarPeriodEntry"]: {
|
||||
dueDate: string,
|
||||
@@ -25899,7 +26041,7 @@ export type ModelTypes = {
|
||||
};
|
||||
["UpdateCallTranscriptionMemoInput"]: {
|
||||
id: string,
|
||||
/** Текст заметки (до 4000 символов) */
|
||||
/** Текст заметки */
|
||||
memo: string
|
||||
};
|
||||
["UpdateChatCoopCalendarEventInput"]: {
|
||||
@@ -29036,6 +29178,16 @@ export type GraphQLTypes = {
|
||||
displayLabel: string,
|
||||
matrixRoomId: string,
|
||||
['...on ChatCoopCalendarRoomOption']: Omit<GraphQLTypes["ChatCoopCalendarRoomOption"], "...on ChatCoopCalendarRoomOption">
|
||||
};
|
||||
["ChatcoopNonProjectCommunicationRoom"]: {
|
||||
__typename: "ChatcoopNonProjectCommunicationRoom",
|
||||
/** Подпись для отображения комнаты */
|
||||
displayLabel: string,
|
||||
/** Тип комнаты (пайщики / совет / секретарь) */
|
||||
kind: GraphQLTypes["NonProjectRoomKind"],
|
||||
/** Идентификатор комнаты Matrix */
|
||||
matrixRoomId: string,
|
||||
['...on ChatcoopNonProjectCommunicationRoom']: Omit<GraphQLTypes["ChatcoopNonProjectCommunicationRoom"], "...on ChatcoopNonProjectCommunicationRoom">
|
||||
};
|
||||
["ChatcoopProjectCommunicationRoom"]: {
|
||||
__typename: "ChatcoopProjectCommunicationRoom",
|
||||
@@ -29057,6 +29209,22 @@ export type GraphQLTypes = {
|
||||
/** origin_server_ts из Matrix (мс) */
|
||||
originServerTs: number,
|
||||
['...on ChatcoopRoomMessageLine']: Omit<GraphQLTypes["ChatcoopRoomMessageLine"], "...on ChatcoopRoomMessageLine">
|
||||
};
|
||||
["ChatcoopSecretaryRoom"]: {
|
||||
__typename: "ChatcoopSecretaryRoom",
|
||||
/** Название комнаты */
|
||||
displayLabel: string,
|
||||
/** Можно ли удалить комнату из интерфейса (только комнаты секретаря) */
|
||||
editable: boolean,
|
||||
/** Комната зашифрована (E2EE) — секретарь не транскрибирует такие комнаты */
|
||||
encrypted: boolean,
|
||||
/** Тип комнаты */
|
||||
kind: GraphQLTypes["ManagedRoomKind"],
|
||||
/** Идентификатор комнаты Matrix */
|
||||
matrixRoomId: string,
|
||||
/** Секретарь присутствует в комнате */
|
||||
secretaryInRoom: boolean,
|
||||
['...on ChatcoopSecretaryRoom']: Omit<GraphQLTypes["ChatcoopSecretaryRoom"], "...on ChatcoopSecretaryRoom">
|
||||
};
|
||||
["CheckMatrixUsernameInput"]: {
|
||||
username: string
|
||||
@@ -29789,6 +29957,12 @@ export type GraphQLTypes = {
|
||||
property_hash: string,
|
||||
/** Имя пользователя */
|
||||
username: string
|
||||
};
|
||||
["CreateSecretaryRoomInput"]: {
|
||||
/** Название комнаты */
|
||||
displayName: string,
|
||||
/** Публичная комната (любой может войти) либо приватная (вход по приглашению создателя) */
|
||||
isPublic: boolean
|
||||
};
|
||||
["CreateSovietIndividualDataInput"]: {
|
||||
/** Дата рождения */
|
||||
@@ -31106,7 +31280,9 @@ export type GraphQLTypes = {
|
||||
['...on IndividualCertificate']: Omit<GraphQLTypes["IndividualCertificate"], "...on IndividualCertificate">
|
||||
};
|
||||
["Init"]: {
|
||||
/** Объект организации кооператива, которая обслуживает данный экземпляр программного обеспечения MONO */
|
||||
/** Признак того, что инициализация выполняется со стороны провайдера. При true coopback ставит init_by_server=true (org_data становится readonly для пользовательского визарда). Поле передаёт provider в callInitSystemMutation. */
|
||||
is_server_init?: boolean | undefined | null,
|
||||
/** Объект организации кооператива, которая обслуживает данный экземпляр программного обеспечения MONO */
|
||||
organization_data: GraphQLTypes["CreateInitOrganizationDataInput"]
|
||||
};
|
||||
["Install"]: {
|
||||
@@ -31238,8 +31414,6 @@ export type GraphQLTypes = {
|
||||
__typename: "Ledger2Wallet",
|
||||
/** Доступный баланс */
|
||||
available: string,
|
||||
/** Заблокированный баланс */
|
||||
blocked: string,
|
||||
/** eosio::name-идентификатор кошелька (w.<contract>.<waltype>) */
|
||||
id: string,
|
||||
/** Название кошелька */
|
||||
@@ -31328,6 +31502,8 @@ export type GraphQLTypes = {
|
||||
/** Имя пользователя */
|
||||
username: string
|
||||
};
|
||||
/** Тип комнаты: пайщики, совет, проект Capital, комната секретаря */
|
||||
["ManagedRoomKind"]: ManagedRoomKind;
|
||||
["MarkReportPeriodInput"]: {
|
||||
mark?: GraphQLTypes["ReportSubmissionMark"] | undefined | null,
|
||||
period?: number | undefined | null,
|
||||
@@ -31891,17 +32067,25 @@ export type GraphQLTypes = {
|
||||
|
||||
Требуемые роли: chairman, member, user. */
|
||||
chatcoopCreateCalendarIcsSubscription: GraphQLTypes["ChatCoopCalendarIcsUrlResponse"],
|
||||
/** Создать комнату с секретарём (публичную или приватную); секретарь подключается сразу
|
||||
|
||||
Требуемые роли: chairman, member. */
|
||||
chatcoopCreateSecretaryRoom: GraphQLTypes["ChatcoopSecretaryRoom"],
|
||||
/** Удалить событие календаря
|
||||
|
||||
Требуемые роли: chairman, member. */
|
||||
chatcoopDeleteCalendarEvent: boolean,
|
||||
/** Удалить комнату секретаря: вывести секретаря и снять комнату с синхронизации (возвращает matrixRoomId)
|
||||
|
||||
Требуемые роли: chairman, member. */
|
||||
chatcoopRemoveSecretaryRoom: string,
|
||||
/** Обновить событие календаря
|
||||
|
||||
Требуемые роли: chairman, member. */
|
||||
chatcoopUpdateCalendarEvent: GraphQLTypes["ChatCoopCalendarEvent"],
|
||||
/** Обновить заметку (memo) к транскрипции звонка
|
||||
|
||||
Требуемые роли: chairman, member, user. */
|
||||
Требуемые роли: chairman, member. */
|
||||
chatcoopUpdateTranscriptionMemo: GraphQLTypes["CallTranscription"],
|
||||
/** Выполнить шаг онбординга capital (создание предложения повестки)
|
||||
|
||||
@@ -32221,6 +32405,8 @@ export type GraphQLTypes = {
|
||||
walmoveWallets: GraphQLTypes["Ledger2AdjustmentResult"],
|
||||
['...on Mutation']: Omit<GraphQLTypes["Mutation"], "...on Mutation">
|
||||
};
|
||||
/** Тип комнаты вне проекта: пайщики, совет, комната секретаря */
|
||||
["NonProjectRoomKind"]: NonProjectRoomKind;
|
||||
["NotificationWorkflowRecipientInput"]: {
|
||||
/** Username получателя */
|
||||
username: string
|
||||
@@ -33161,8 +33347,6 @@ export type GraphQLTypes = {
|
||||
available: string,
|
||||
/** Номер блока последнего обновления */
|
||||
blockNum?: number | undefined | null,
|
||||
/** Заблокированный баланс (формат: "100.0000 RUB") */
|
||||
blocked: string,
|
||||
/** Имя кооператива */
|
||||
coopname: string,
|
||||
/** Уникальный идентификатор кошелька в блокчейне */
|
||||
@@ -33514,10 +33698,18 @@ export type GraphQLTypes = {
|
||||
|
||||
Требуемые роли: chairman, member. */
|
||||
chatcoopListCalendarRooms: Array<GraphQLTypes["ChatCoopCalendarRoomOption"]>,
|
||||
/** Комнаты Matrix вне проектов Capital (пайщики/совет/секретарь) — для синхронизации в blago
|
||||
|
||||
Требуемые роли: chairman, member, user. */
|
||||
chatcoopListNonProjectCommunicationRooms: Array<GraphQLTypes["ChatcoopNonProjectCommunicationRoom"]>,
|
||||
/** Комнаты Matrix, привязанные к проекту Capital (реестр ChatCoop)
|
||||
|
||||
Требуемые роли: chairman, member, user. */
|
||||
chatcoopListProjectCommunicationRooms: Array<GraphQLTypes["ChatcoopProjectCommunicationRoom"]>,
|
||||
/** Все комнаты реестра ChatCoop (системные/проектные — read-only, комнаты секретаря — удаляемые)
|
||||
|
||||
Требуемые роли: chairman, member. */
|
||||
chatcoopListSecretaryRooms: Array<GraphQLTypes["ChatcoopSecretaryRoom"]>,
|
||||
/** UTC-даты (YYYY-MM-DD), в которых есть сообщения новее afterOriginServerTsExclusive, для комнаты Matrix
|
||||
|
||||
Требуемые роли: chairman, member, user. */
|
||||
@@ -33903,6 +34095,10 @@ export type GraphQLTypes = {
|
||||
/** Название программы для отображения */
|
||||
title: string,
|
||||
['...on RegistrationProgram']: Omit<GraphQLTypes["RegistrationProgram"], "...on RegistrationProgram">
|
||||
};
|
||||
["RemoveSecretaryRoomInput"]: {
|
||||
/** Идентификатор комнаты Matrix, которую нужно удалить */
|
||||
matrixRoomId: string
|
||||
};
|
||||
["ReportCalendarPeriodEntry"]: {
|
||||
__typename: "ReportCalendarPeriodEntry",
|
||||
@@ -34973,7 +35169,7 @@ export type GraphQLTypes = {
|
||||
};
|
||||
["UpdateCallTranscriptionMemoInput"]: {
|
||||
id: string,
|
||||
/** Текст заметки (до 4000 символов) */
|
||||
/** Текст заметки */
|
||||
memo: string
|
||||
};
|
||||
["UpdateChatCoopCalendarEventInput"]: {
|
||||
@@ -35597,6 +35793,19 @@ export enum LogEventType {
|
||||
VOTING_COMPLETED = "VOTING_COMPLETED",
|
||||
VOTING_STARTED = "VOTING_STARTED"
|
||||
}
|
||||
/** Тип комнаты: пайщики, совет, проект Capital, комната секретаря */
|
||||
export enum ManagedRoomKind {
|
||||
CAPITAL_PROJECT = "CAPITAL_PROJECT",
|
||||
COUNCIL = "COUNCIL",
|
||||
MEMBERS = "MEMBERS",
|
||||
SECRETARY = "SECRETARY"
|
||||
}
|
||||
/** Тип комнаты вне проекта: пайщики, совет, комната секретаря */
|
||||
export enum NonProjectRoomKind {
|
||||
COUNCIL = "COUNCIL",
|
||||
MEMBERS = "MEMBERS",
|
||||
SECRETARY = "SECRETARY"
|
||||
}
|
||||
/** Тип юридического лица */
|
||||
export enum OrganizationType {
|
||||
AO = "AO",
|
||||
@@ -35614,6 +35823,7 @@ export enum PaymentDirection {
|
||||
}
|
||||
/** Статус платежа */
|
||||
export enum PaymentStatus {
|
||||
AWAITING_AUTHORIZATION = "AWAITING_AUTHORIZATION",
|
||||
CANCELLED = "CANCELLED",
|
||||
COMPLETED = "COMPLETED",
|
||||
EXPIRED = "EXPIRED",
|
||||
@@ -35863,6 +36073,7 @@ type ZEUS_VARIABLES = {
|
||||
["CreateProjectInput"]: ValueTypes["CreateProjectInput"];
|
||||
["CreateProjectInvestInput"]: ValueTypes["CreateProjectInvestInput"];
|
||||
["CreateProjectPropertyInput"]: ValueTypes["CreateProjectPropertyInput"];
|
||||
["CreateSecretaryRoomInput"]: ValueTypes["CreateSecretaryRoomInput"];
|
||||
["CreateSovietIndividualDataInput"]: ValueTypes["CreateSovietIndividualDataInput"];
|
||||
["CreateStoryInput"]: ValueTypes["CreateStoryInput"];
|
||||
["CreateSubscriptionInput"]: ValueTypes["CreateSubscriptionInput"];
|
||||
@@ -35960,9 +36171,11 @@ type ZEUS_VARIABLES = {
|
||||
["LoginInput"]: ValueTypes["LoginInput"];
|
||||
["LogoutInput"]: ValueTypes["LogoutInput"];
|
||||
["MakeClearanceInput"]: ValueTypes["MakeClearanceInput"];
|
||||
["ManagedRoomKind"]: ValueTypes["ManagedRoomKind"];
|
||||
["MarkReportPeriodInput"]: ValueTypes["MarkReportPeriodInput"];
|
||||
["ModerateRequestInput"]: ValueTypes["ModerateRequestInput"];
|
||||
["MoveCapitalIssueToComponentInput"]: ValueTypes["MoveCapitalIssueToComponentInput"];
|
||||
["NonProjectRoomKind"]: ValueTypes["NonProjectRoomKind"];
|
||||
["NotificationWorkflowRecipientInput"]: ValueTypes["NotificationWorkflowRecipientInput"];
|
||||
["NotifyOnAnnualGeneralMeetInput"]: ValueTypes["NotifyOnAnnualGeneralMeetInput"];
|
||||
["OpenProjectInput"]: ValueTypes["OpenProjectInput"];
|
||||
@@ -36008,6 +36221,7 @@ type ZEUS_VARIABLES = {
|
||||
["RegisterAccountInput"]: ValueTypes["RegisterAccountInput"];
|
||||
["RegisterContributorInput"]: ValueTypes["RegisterContributorInput"];
|
||||
["RegisterParticipantInput"]: ValueTypes["RegisterParticipantInput"];
|
||||
["RemoveSecretaryRoomInput"]: ValueTypes["RemoveSecretaryRoomInput"];
|
||||
["ReportHistoryFilterInput"]: ValueTypes["ReportHistoryFilterInput"];
|
||||
["ReportPreviewInput"]: ValueTypes["ReportPreviewInput"];
|
||||
["ReportSubmissionMark"]: ValueTypes["ReportSubmissionMark"];
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "cooptypes",
|
||||
"type": "module",
|
||||
"version": "2026.5.23-4",
|
||||
"version": "2026.5.25-3",
|
||||
"description": "Shared TypeScript типы и интерфейсы экосистемы кооперативов",
|
||||
"author": "Alex Ant <dacom.dark.sun@gmail.com>",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -11,11 +11,11 @@
|
||||
*
|
||||
* Идентификаторы кошельков (`wallet_from`/`wallet_to`) — eosio::name с
|
||||
* префиксом `w.<contract>.<waltype>` (см. `./wallets.ts`). Пустая строка
|
||||
* (`""`) — sentinel «кошелёк вне системы» для ISSUE, BURN и BURN_BLOCKED.
|
||||
* (`""`) — sentinel «кошелёк вне системы» для ISSUE и BURN.
|
||||
*/
|
||||
import type { IName } from '../interfaces/ledger2'
|
||||
|
||||
export type WalletOp = 'ISSUE' | 'TRANSFER' | 'BLOCK' | 'UNBLOCK' | 'BURN' | 'BURN_BLOCKED' | 'NONE'
|
||||
export type WalletOp = 'ISSUE' | 'TRANSFER' | 'BURN' | 'NONE'
|
||||
|
||||
export interface OperationMeta {
|
||||
/** Машинный идентификатор — eosio::name в контракте. */
|
||||
@@ -34,7 +34,7 @@ export interface OperationMeta {
|
||||
wallet_op: WalletOp | null
|
||||
/** Кошелёк-источник (null для ISSUE и для adjustment-операций). */
|
||||
wallet_from: IName | null
|
||||
/** Кошелёк-приёмник (null для BLOCK/UNBLOCK/BURN/BURN_BLOCKED и для adjustment-операций). */
|
||||
/** Кошелёк-приёмник (null для BURN и для adjustment-операций). */
|
||||
wallet_to: IName | null
|
||||
/** Код счёта Дт (null без бухпроводки, ADR-003: ⇔ credit == null). */
|
||||
debit: number | null
|
||||
@@ -74,17 +74,17 @@ export const LEDGER2_OPERATION_REGISTRY: readonly OperationMeta[] = [
|
||||
human_name: 'Внесение пайщиком паевого взноса' },
|
||||
|
||||
{ code: 'o.wal.wthreq', process_type: 'p.wal.wthdrw', contract: 'wallet',
|
||||
name: 'REQUEST_WITHDRAW', wallet_op: 'BLOCK', wallet_from: 'w.wal.share', wallet_to: null,
|
||||
name: 'REQUEST_WITHDRAW', wallet_op: 'TRANSFER', wallet_from: 'w.wal.share', wallet_to: 'w.wal.wpend',
|
||||
debit: null, credit: null,
|
||||
human_name: 'Блокировка паевого под запрос на возврат' },
|
||||
human_name: 'Резервирование паевого под запрос на возврат' },
|
||||
|
||||
{ code: 'o.wal.wthdec', process_type: 'p.wal.wthdrw', contract: 'wallet',
|
||||
name: 'DECLINE_WITHDRAW', wallet_op: 'UNBLOCK', wallet_from: 'w.wal.share', wallet_to: null,
|
||||
name: 'DECLINE_WITHDRAW', wallet_op: 'TRANSFER', wallet_from: 'w.wal.wpend', wallet_to: 'w.wal.share',
|
||||
debit: null, credit: null,
|
||||
human_name: 'Разблокировка паевого после отклонения запроса на возврат' },
|
||||
human_name: 'Снятие резерва паевого после отклонения запроса на возврат' },
|
||||
|
||||
{ code: 'o.wal.wthcpl', process_type: 'p.wal.wthdrw', contract: 'wallet',
|
||||
name: 'COMPLETE_WITHDRAW', wallet_op: 'BURN_BLOCKED', wallet_from: 'w.wal.share', wallet_to: null,
|
||||
name: 'COMPLETE_WITHDRAW', wallet_op: 'BURN', wallet_from: 'w.wal.wpend', wallet_to: null,
|
||||
debit: 80, credit: 51,
|
||||
human_name: 'Возврат паевого взноса пайщику' },
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ export const LEDGER2_WALLET_REGISTRY: readonly WalletMeta[] = [
|
||||
{ name: "w.cap.gen", human_name: "ЦПП «Генератор» — единый кошелёк программы", kind: "COOPERATIVE" },
|
||||
{ name: "w.reg.entry", human_name: "Вступительные взносы", kind: "COOPERATIVE" },
|
||||
{ name: "w.wal.wthdrw", human_name: "Возвраты паевых взносов пайщикам (deprecated, не используется в новых операциях)", kind: "COOPERATIVE" },
|
||||
{ name: "w.wal.wpend", human_name: "Резерв паевого под заявку на возврат", kind: "COOPERATIVE" },
|
||||
{ name: "w.sov.infra", human_name: "Членские взносы за инфраструктуру кооп. платформы", kind: "COOPERATIVE" },
|
||||
{ name: "w.sov.delgte", human_name: "Делегатские членские взносы", kind: "COOPERATIVE" },
|
||||
{ name: "w.sov.expns", human_name: "Хозяйственные расходы из числа целевого финансирования", kind: "COOPERATIVE" },
|
||||
|
||||
+2
-5
@@ -33,7 +33,6 @@ import { computed } from 'vue';
|
||||
import { useWalletStore } from 'src/entities/Wallet/model';
|
||||
import { ColorCard } from 'src/shared/ui';
|
||||
import { formatAsset2Digits } from 'src/shared/lib/utils/formatAsset2Digits';
|
||||
import { addAssets } from 'src/shared/lib/utils/addAssets';
|
||||
import { Zeus } from '@coopenomics/sdk';
|
||||
import { CreateProgramInvestButton } from 'app/extensions/capital/features/ProgramInvest/CreateProgramInvest/ui';
|
||||
|
||||
@@ -47,8 +46,7 @@ const generatorWallet = computed(() => {
|
||||
|
||||
return {
|
||||
available: wallet?.available || '0.00',
|
||||
blocked: wallet?.blocked || '0.00',
|
||||
total: addAssets(wallet?.available || '0.00', wallet?.blocked || '0.00'),
|
||||
total: wallet?.available || '0.00',
|
||||
};
|
||||
});
|
||||
|
||||
@@ -61,8 +59,7 @@ const blagorostWallet = computed(() => {
|
||||
|
||||
return {
|
||||
available: wallet?.available || '0.00',
|
||||
blocked: wallet?.blocked || '0.00',
|
||||
total: addAssets(wallet?.available || '0.00', wallet?.blocked || '0.00'),
|
||||
total: wallet?.available || '0.00',
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
+9
-24
@@ -244,45 +244,30 @@ const calculateInvestorTotal = (contributor: IContributor) => {
|
||||
// Функции для расчета сумм кошельков
|
||||
const calculateMainWalletTotal = (contributor: IContributor) => {
|
||||
const availableStr = contributor?.main_wallet?.available || '0.0000';
|
||||
const blockedStr = contributor?.main_wallet?.blocked || '0.0000';
|
||||
const total = Number(availableStr.split(' ')[0] || '0');
|
||||
|
||||
const available = Number(availableStr.split(' ')[0] || '0');
|
||||
const blocked = Number(blockedStr.split(' ')[0] || '0');
|
||||
|
||||
const total = available + blocked;
|
||||
|
||||
// Определяем валюту из любого поля, которое ее содержит, или используем системный символ
|
||||
const currency = availableStr.split(' ')[1] || blockedStr.split(' ')[1] || governSymbol.value;
|
||||
// Определяем валюту из поля available или используем системный символ
|
||||
const currency = availableStr.split(' ')[1] || governSymbol.value;
|
||||
|
||||
return currency ? `${total} ${currency}` : total.toString();
|
||||
};
|
||||
|
||||
const calculateGenerationWalletTotal = (contributor: IContributor) => {
|
||||
const availableStr = contributor?.generation_wallet?.available || '0.0000';
|
||||
const blockedStr = contributor?.generation_wallet?.blocked || '0.0000';
|
||||
const total = Number(availableStr.split(' ')[0] || '0');
|
||||
|
||||
const available = Number(availableStr.split(' ')[0] || '0');
|
||||
const blocked = Number(blockedStr.split(' ')[0] || '0');
|
||||
|
||||
const total = available + blocked;
|
||||
|
||||
// Определяем валюту из любого поля, которое ее содержит, или используем системный символ
|
||||
const currency = availableStr.split(' ')[1] || blockedStr.split(' ')[1] || governSymbol.value;
|
||||
// Определяем валюту из поля available или используем системный символ
|
||||
const currency = availableStr.split(' ')[1] || governSymbol.value;
|
||||
|
||||
return currency ? `${total} ${currency}` : total.toString();
|
||||
};
|
||||
|
||||
const calculateBlagorostWalletTotal = (contributor: IContributor) => {
|
||||
const availableStr = contributor?.blagorost_wallet?.available || '0.0000';
|
||||
const blockedStr = contributor?.blagorost_wallet?.blocked || '0.0000';
|
||||
const total = Number(availableStr.split(' ')[0] || '0');
|
||||
|
||||
const available = Number(availableStr.split(' ')[0] || '0');
|
||||
const blocked = Number(blockedStr.split(' ')[0] || '0');
|
||||
|
||||
const total = available + blocked;
|
||||
|
||||
// Определяем валюту из любого поля, которое ее содержит, или используем системный символ
|
||||
const currency = availableStr.split(' ')[1] || blockedStr.split(' ')[1] || governSymbol.value;
|
||||
// Определяем валюту из поля available или используем системный символ
|
||||
const currency = availableStr.split(' ')[1] || governSymbol.value;
|
||||
|
||||
return currency ? `${total} ${currency}` : total.toString();
|
||||
};
|
||||
|
||||
@@ -106,8 +106,8 @@ export const useOnboardingFlow = () => {
|
||||
const importStep: AgendaStep = {
|
||||
key: 'import_participants',
|
||||
type: 'import',
|
||||
title: 'Импорт пайщиков',
|
||||
description: 'Загрузите пайщиков через CSV-файл.',
|
||||
title: 'Импорт пайщиков (опционально)',
|
||||
description: 'Необязательный шаг. Если у вас уже есть пайщики кооператива — загрузите их списком через CSV-файл. Если нет — просто двигайтесь дальше.',
|
||||
done: false,
|
||||
pending: false,
|
||||
};
|
||||
|
||||
+216
-93
@@ -11,92 +11,122 @@ OnboardingCompletionCelebration(
|
||||
)
|
||||
|
||||
// Показываем шаги если онбординг не завершен и данные загружены
|
||||
q-card(v-else flat)
|
||||
q-card-section.row.items-center.justify-between
|
||||
div
|
||||
div.text-h5 Адаптируйте кооператив к работе на платформе
|
||||
q-chip(
|
||||
.onboarding(v-else)
|
||||
.onboarding__head
|
||||
h2.onboarding__title Адаптируйте кооператив к работе на платформе
|
||||
p.onboarding__lead
|
||||
| Чтобы кооператив начал работать в цифровом контуре платформы, совет
|
||||
| принимает несколько решений в электронной форме — по одному на каждый
|
||||
| шаг ниже. Сначала утверждаются положения о цифровых сервисах и формы
|
||||
| документов кооператива, затем оформляется вступление в Потребительский
|
||||
| кооператив «Восход». После этого вы импортируете пайщиков и объявляете
|
||||
| общее собрание. Для каждого шага платформа сформирует проект повестки и
|
||||
| зафиксирует принятое решение — пройдите все шаги до окончания срока
|
||||
| адаптации.
|
||||
.onboarding__deadline
|
||||
q-icon(name="fa-regular fa-clock" size="15px")
|
||||
span {{ countdownLabel ? `Срок адаптации: ${countdownLabel}` : 'Отсчёт появится после получения данных' }}
|
||||
|
||||
color="primary"
|
||||
text-color="white"
|
||||
icon="schedule"
|
||||
) {{ countdownLabel || 'Отсчёт появится после получения данных' }}
|
||||
q-separator
|
||||
q-card-section
|
||||
q-list(separator)
|
||||
q-item(v-for="(step, index) in steps" :key="step.key")
|
||||
q-item-section
|
||||
div.row.items-center.q-gutter-sm
|
||||
q-icon(
|
||||
:name="getIcon(step)"
|
||||
:color="getIconColor(step)"
|
||||
size="22px"
|
||||
q-separator.onboarding__sep
|
||||
|
||||
.stepper.stepper--v
|
||||
.step(
|
||||
v-for="(step, index) in steps"
|
||||
:key="step.key"
|
||||
:class="stepClass(index)"
|
||||
)
|
||||
.step__rail
|
||||
.step__num
|
||||
q-icon(v-if="step.done" name="fa-solid fa-check")
|
||||
template(v-else) {{ index + 1 }}
|
||||
.step__line
|
||||
.step__body
|
||||
.step__label {{ step.title }}
|
||||
.step__sublabel {{ step.description }}
|
||||
.step__meta(v-if="!step.done && step.pending")
|
||||
q-icon(name="fa-solid fa-hourglass-half" size="12px")
|
||||
span {{ getPendingText(step) }}
|
||||
|
||||
.step__action(v-if="showAction(index)")
|
||||
BaseButton(
|
||||
v-if="step.type === 'agenda'"
|
||||
variant="primary"
|
||||
size="sm"
|
||||
:loading="loadingAction"
|
||||
@click="openAgendaDialog(step)"
|
||||
)
|
||||
div.text-subtitle1 {{ index + 1 }}. {{ step.title }}
|
||||
div.text-caption.text-grey-7 {{ step.description }}
|
||||
q-chip(
|
||||
v-if="!step.done && step.pending"
|
||||
dense
|
||||
color="amber"
|
||||
text-color="black"
|
||||
icon="hourglass_top"
|
||||
class="q-mt-xs"
|
||||
) {{ getPendingText(step) }}
|
||||
q-item-section(side)
|
||||
q-btn(
|
||||
v-if="step.type === 'agenda' && showAction(index)"
|
||||
:disable="loadingAction"
|
||||
color="primary"
|
||||
label="Объявить собрание совета"
|
||||
@click="openAgendaDialog(step)"
|
||||
)
|
||||
template(#icon-left)
|
||||
q-icon(name="fa-solid fa-bullhorn" size="14px")
|
||||
| Объявить собрание совета
|
||||
|
||||
q-btn(
|
||||
v-else-if="step.type === 'import' && showAction(index)"
|
||||
:disable="loadingAction || isActionDisabled(index)"
|
||||
color="primary"
|
||||
icon="upload_file"
|
||||
push
|
||||
label="Импорт пайщиков"
|
||||
@click="importDialog = true"
|
||||
)
|
||||
q-tooltip(
|
||||
v-if="isActionDisabled(index)"
|
||||
anchor="top middle"
|
||||
self="bottom middle"
|
||||
) Для продолжения примите решения советом
|
||||
q-btn(
|
||||
v-else-if="step.type === 'meet' && showAction(index)"
|
||||
:disable="loadingAction || isActionDisabled(index)"
|
||||
color="primary"
|
||||
label="Объявить общее собрание"
|
||||
@click="openMeetDialog"
|
||||
)
|
||||
q-tooltip(
|
||||
v-if="isActionDisabled(index)"
|
||||
anchor="top middle"
|
||||
self="bottom middle"
|
||||
) Для продолжения примите решения советом
|
||||
BaseButton(
|
||||
v-else-if="step.type === 'import'"
|
||||
variant="primary"
|
||||
size="sm"
|
||||
:disabled="loadingAction || isActionDisabled(index)"
|
||||
@click="importDialog = true"
|
||||
)
|
||||
template(#icon-left)
|
||||
q-icon(name="fa-solid fa-file-arrow-up" size="14px")
|
||||
| Импорт пайщиков
|
||||
q-tooltip(
|
||||
v-if="isActionDisabled(index)"
|
||||
anchor="top middle"
|
||||
self="bottom middle"
|
||||
) Для продолжения примите решения советом
|
||||
|
||||
q-dialog(v-model="agendaDialog.open" persistent)
|
||||
ModalBase(title="Предложение повестки" @close="closeAgendaDialog" style="min-width: 640px")
|
||||
template(#default)
|
||||
q-card-section
|
||||
div.row.items-center.q-gutter-xs.text-subtitle1.text-weight-medium
|
||||
q-icon(name="help_outline" size="18px" class="text-primary")
|
||||
span Вопрос на повестке
|
||||
div.q-mt-sm.q-pa-sm.text-body1.rounded-borders {{ agendaDialog.question }}
|
||||
BaseButton(
|
||||
v-else-if="step.type === 'meet'"
|
||||
variant="primary"
|
||||
size="sm"
|
||||
:disabled="loadingAction || isActionDisabled(index)"
|
||||
@click="openMeetDialog"
|
||||
)
|
||||
template(#icon-left)
|
||||
q-icon(name="fa-solid fa-users" size="14px")
|
||||
| Объявить общее собрание
|
||||
q-tooltip(
|
||||
v-if="isActionDisabled(index)"
|
||||
anchor="top middle"
|
||||
self="bottom middle"
|
||||
) Для продолжения примите решения советом
|
||||
|
||||
q-separator.q-my-md
|
||||
BaseDialog(
|
||||
:model-value="agendaDialog.open"
|
||||
size="lg"
|
||||
:title="agendaDialog.title || 'Предложение повестки'"
|
||||
:close-on-backdrop="!loadingAction"
|
||||
:close-on-escape="!loadingAction"
|
||||
@update:model-value="onAgendaDialogToggle"
|
||||
)
|
||||
.agenda-dialog
|
||||
.agenda-dialog__section
|
||||
.agenda-dialog__label
|
||||
q-icon(name="fa-regular fa-circle-question" size="16px")
|
||||
span Вопрос на повестке
|
||||
.agenda-dialog__question {{ agendaDialog.question }}
|
||||
|
||||
div.row.items-center.q-gutter-xs.text-subtitle1.text-weight-medium
|
||||
q-icon(name="gavel" size="18px" class="text-primary")
|
||||
span Проект решения
|
||||
div.q-mt-sm.q-pa-sm.rounded-borders
|
||||
DocumentHtmlReader(:html="agendaDialog.decision" :sanitize="false")
|
||||
div.q-pb-lg
|
||||
q-btn(flat label="Отмена" @click="closeAgendaDialog" :disable="loadingAction")
|
||||
q-btn(color="primary" label="Объявить" :loading="loadingAction" @click="submitAgenda")
|
||||
.agenda-dialog__section
|
||||
.agenda-dialog__label
|
||||
q-icon(name="fa-solid fa-gavel" size="16px")
|
||||
span Проект решения
|
||||
.agenda-dialog__decision
|
||||
DocumentHtmlReader(:html="agendaDialog.decision" :sanitize="false")
|
||||
|
||||
template(#footer)
|
||||
BaseButton(
|
||||
variant="ghost"
|
||||
:disabled="loadingAction"
|
||||
@click="closeAgendaDialog"
|
||||
) Отмена
|
||||
BaseButton(
|
||||
variant="primary"
|
||||
:loading="loadingAction"
|
||||
@click="submitAgenda"
|
||||
)
|
||||
template(#icon-left)
|
||||
q-icon(name="fa-solid fa-bullhorn" size="15px")
|
||||
| Объявить
|
||||
|
||||
CreateMeetForm(
|
||||
v-model="meetDialog"
|
||||
@@ -114,8 +144,9 @@ import { onMounted, ref } from 'vue';
|
||||
import { CreateMeetForm } from 'src/features/Meet/CreateMeet/ui';
|
||||
import { ParticipantsImportDialog } from 'src/features/User/ImportParticipants/ui';
|
||||
import { DocumentHtmlReader } from 'src/shared/ui/DocumentHtmlReader';
|
||||
import { ModalBase } from 'src/shared/ui/ModalBase';
|
||||
import { WindowLoader } from 'src/shared/ui/Loader';
|
||||
import { BaseButton } from 'src/shared/ui/base/BaseButton';
|
||||
import { BaseDialog } from 'src/shared/ui/base/BaseDialog';
|
||||
import OnboardingCompletionCelebration from './OnboardingCompletionCelebration.vue';
|
||||
import { useOnboardingFlow } from '../model';
|
||||
|
||||
@@ -138,18 +169,11 @@ const {
|
||||
|
||||
const importDialog = ref(false);
|
||||
|
||||
const getIcon = (step: { done: boolean; pending: boolean; type: string }) => {
|
||||
if (step.done) return 'task_alt';
|
||||
if (step.pending) return 'hourglass_top';
|
||||
if (step.type === 'import') return 'arrow_forward';
|
||||
return 'radio_button_unchecked';
|
||||
};
|
||||
|
||||
const getIconColor = (step: { done: boolean; pending: boolean; type: string }) => {
|
||||
if (step.done) return 'green-6';
|
||||
if (step.pending) return 'orange-6';
|
||||
if (step.type === 'import') return '';
|
||||
return 'grey-6';
|
||||
// Закрытие диалога повестки (крестик / клик по фону / Escape) сбрасывает
|
||||
// его состояние через closeAgendaDialog. Открытие идёт только через
|
||||
// кнопку шага (openAgendaDialog), поэтому здесь обрабатываем лишь закрытие.
|
||||
const onAgendaDialogToggle = (open: boolean) => {
|
||||
if (!open) closeAgendaDialog();
|
||||
};
|
||||
|
||||
const getPendingText = (step: { type: string }) => {
|
||||
@@ -191,5 +215,104 @@ const isActionDisabled = (index: number) => {
|
||||
return false;
|
||||
};
|
||||
|
||||
// Канон-степпер: пройденный шаг — is-done (галочка, заливка primary),
|
||||
// текущий доступный к действию — is-active (подсветка номера). Ожидающие
|
||||
// решения и ещё недоступные шаги — нейтральные.
|
||||
const stepClass = (index: number) => {
|
||||
const step = steps.value[index];
|
||||
if (!step) return '';
|
||||
if (step.done) return 'is-done';
|
||||
if (showAction(index)) return 'is-active';
|
||||
return '';
|
||||
};
|
||||
|
||||
onMounted(init);
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.onboarding {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.onboarding__head {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--p-3, 12px);
|
||||
}
|
||||
.onboarding__title {
|
||||
margin: 0;
|
||||
font-size: var(--p-fs-h2);
|
||||
font-weight: 600;
|
||||
letter-spacing: -0.01em;
|
||||
color: var(--p-ink);
|
||||
}
|
||||
.onboarding__lead {
|
||||
margin: 0;
|
||||
font-size: var(--p-fs-body);
|
||||
line-height: 1.55;
|
||||
color: var(--p-ink-2);
|
||||
}
|
||||
// Разделитель между шапкой и степпером — канон-воздух сверху/снизу.
|
||||
.onboarding__sep {
|
||||
margin: var(--p-5, 20px) 0;
|
||||
}
|
||||
// Действие шага — без обрамляющей рамки, просто отступ сверху.
|
||||
.step__action {
|
||||
margin-top: var(--p-3, 12px);
|
||||
}
|
||||
// Срок адаптации — спокойная плашка-предупреждение, отсчёт виден сразу.
|
||||
.onboarding__deadline {
|
||||
align-self: flex-start;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 6px 12px;
|
||||
border-radius: var(--p-r-pill, 999px);
|
||||
background: var(--p-warn-soft);
|
||||
color: var(--p-warn);
|
||||
font-size: var(--p-fs-body-sm);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
// Диалог повестки.
|
||||
.agenda-dialog {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--p-5, 20px);
|
||||
}
|
||||
.agenda-dialog__section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--p-2, 8px);
|
||||
}
|
||||
// Подписи секций — надстрочные метки (eyebrow): отдельный типографический
|
||||
// ярус ниже заголовка окна и заметно отличный от него, чтобы не казаться
|
||||
// конкурирующим заголовком на фоне крупного заголовка самого документа.
|
||||
.agenda-dialog__label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
font-size: var(--p-fs-body-sm);
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
color: var(--p-ink-3);
|
||||
|
||||
.q-icon {
|
||||
color: var(--p-primary);
|
||||
}
|
||||
}
|
||||
.agenda-dialog__question {
|
||||
font-size: var(--p-fs-body);
|
||||
line-height: 1.5;
|
||||
color: var(--p-ink);
|
||||
}
|
||||
.agenda-dialog__decision {
|
||||
padding: var(--p-4, 16px);
|
||||
background: var(--p-surface-2);
|
||||
border: 1px solid var(--p-line);
|
||||
border-radius: var(--p-r-sm, 8px);
|
||||
max-height: 50vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
</style>
|
||||
|
||||
+53
-39
@@ -1,43 +1,41 @@
|
||||
<template lang="pug">
|
||||
form.full-width(@submit.prevent='submit')
|
||||
.q-gutter-md
|
||||
// Провайдер платежей по умолчанию
|
||||
.row.q-gutter-md
|
||||
.col-md-6.col-sm-12
|
||||
q-select(
|
||||
v-model='formData.provider_name'
|
||||
:options='providerOptions'
|
||||
label='Провайдер входящих платежей'
|
||||
placeholder='Выберите провайдера'
|
||||
dense
|
||||
standout="bg-teal text-white"
|
||||
emit-value
|
||||
map-options
|
||||
option-value='value'
|
||||
option-label='label'
|
||||
:loading='loading'
|
||||
)
|
||||
form.pp-form(@submit.prevent='submit')
|
||||
q-select.pp-form__select(
|
||||
v-model='formData.provider_name'
|
||||
:options='providerOptions'
|
||||
label='Провайдер входящих платежей'
|
||||
placeholder='Выберите провайдера'
|
||||
dense
|
||||
outlined
|
||||
color='primary'
|
||||
emit-value
|
||||
map-options
|
||||
option-value='value'
|
||||
option-label='label'
|
||||
:loading='loading'
|
||||
)
|
||||
|
||||
.col-md-12(v-if='selectedProviderDescription')
|
||||
.q-mt-sm.text-caption.text-grey-7 {{ selectedProviderDescription }}
|
||||
.pp-form__hint(v-if='selectedProviderDescription') {{ selectedProviderDescription }}
|
||||
|
||||
.q-mt-md
|
||||
.row.justify-start.q-gutter-sm
|
||||
q-btn(
|
||||
flat
|
||||
label='Отменить'
|
||||
color='grey'
|
||||
@click='resetForm'
|
||||
:loading='loading'
|
||||
:disable='!hasChanges'
|
||||
)
|
||||
q-btn(
|
||||
type='submit'
|
||||
label='Сохранить'
|
||||
color='primary'
|
||||
:loading='loading'
|
||||
:disable='!hasChanges'
|
||||
)
|
||||
.pp-form__actions
|
||||
q-btn(
|
||||
flat
|
||||
no-caps
|
||||
label='Отменить'
|
||||
color='grey-7'
|
||||
@click='resetForm'
|
||||
:loading='loading'
|
||||
:disable='!hasChanges'
|
||||
)
|
||||
q-btn(
|
||||
type='submit'
|
||||
no-caps
|
||||
unelevated
|
||||
label='Сохранить'
|
||||
color='primary'
|
||||
:loading='loading'
|
||||
:disable='!hasChanges'
|
||||
)
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
@@ -133,8 +131,24 @@ const submit = async () => {
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.full-width {
|
||||
<style scoped lang="scss">
|
||||
.pp-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--p-4, 16px);
|
||||
width: 100%;
|
||||
}
|
||||
.pp-form__select {
|
||||
max-width: 480px;
|
||||
}
|
||||
.pp-form__hint {
|
||||
max-width: 640px;
|
||||
font-size: var(--p-fs-body-sm);
|
||||
line-height: 1.5;
|
||||
color: var(--p-ink-2);
|
||||
}
|
||||
.pp-form__actions {
|
||||
display: flex;
|
||||
gap: var(--p-2, 8px);
|
||||
}
|
||||
</style>
|
||||
|
||||
+8
-4
@@ -10,7 +10,8 @@ form.full-width(@submit.prevent='submit')
|
||||
label='Рабочий стол для авторизованных пользователей'
|
||||
placeholder='Выберите рабочий стол'
|
||||
dense
|
||||
standout="bg-teal text-white"
|
||||
outlined
|
||||
color="primary"
|
||||
emit-value
|
||||
map-options
|
||||
option-value='value'
|
||||
@@ -26,7 +27,8 @@ form.full-width(@submit.prevent='submit')
|
||||
label='Страница для авторизованных пользователей'
|
||||
placeholder='Выберите страницу'
|
||||
dense
|
||||
standout="bg-teal text-white"
|
||||
outlined
|
||||
color="primary"
|
||||
emit-value
|
||||
map-options
|
||||
option-value='value'
|
||||
@@ -45,7 +47,8 @@ form.full-width(@submit.prevent='submit')
|
||||
label='Рабочий стол для неавторизованных пользователей'
|
||||
placeholder='Выберите рабочий стол'
|
||||
dense
|
||||
standout="bg-teal text-white"
|
||||
outlined
|
||||
color="primary"
|
||||
emit-value
|
||||
map-options
|
||||
option-value='value'
|
||||
@@ -61,7 +64,8 @@ form.full-width(@submit.prevent='submit')
|
||||
label='Страница для неавторизованных пользователей'
|
||||
placeholder='Выберите страницу'
|
||||
dense
|
||||
standout="bg-teal text-white"
|
||||
outlined
|
||||
color="primary"
|
||||
emit-value
|
||||
map-options
|
||||
option-value='value'
|
||||
|
||||
@@ -77,7 +77,7 @@ export default async function (): Promise<IWorkspaceConfig[]> {
|
||||
name: 'extensions',
|
||||
component: markRaw(ExtensionsManagement),
|
||||
meta: {
|
||||
title: 'Магазин приложений',
|
||||
title: 'Каталог приложений',
|
||||
icon: 'fa-solid fa-puzzle-piece',
|
||||
roles: ['chairman'],
|
||||
agreements: agreementsBase,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template lang="pug">
|
||||
div
|
||||
.approvals-page
|
||||
q-card(flat)
|
||||
|
||||
// Фильтры
|
||||
@@ -11,7 +11,8 @@ div
|
||||
label='Статус'
|
||||
placeholder='без фильтра'
|
||||
dense
|
||||
standout="bg-teal text-white"
|
||||
outlined
|
||||
color="primary"
|
||||
@update:model-value='onFiltersChange'
|
||||
style="width: 250px"
|
||||
)
|
||||
@@ -128,3 +129,12 @@ watch(() => route.params.coopname, () => {
|
||||
loadApprovals();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.approvals-page {
|
||||
padding: var(--p-6, 24px);
|
||||
@media (max-width: 768px) {
|
||||
padding: var(--p-4, 16px);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,8 +1,18 @@
|
||||
<template lang="pug">
|
||||
q-page.padding
|
||||
q-page.onboarding-page
|
||||
OnboardingStepsCard
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { OnboardingStepsCard } from 'app/extensions/chairman/features/Onboarding/ui';
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
// Канон-отступы страницы (как у прочих столов): контент не липнет к краям.
|
||||
.onboarding-page {
|
||||
padding: var(--p-6, 24px);
|
||||
@media (max-width: 768px) {
|
||||
padding: var(--p-4, 16px);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
+18
-33
@@ -1,17 +1,19 @@
|
||||
<template lang="pug">
|
||||
div.page-shell
|
||||
q-card.hero-card(flat)
|
||||
.hero-title Провайдер платежей
|
||||
.hero-subtitle
|
||||
.payment-provider-page
|
||||
.banner
|
||||
q-icon.banner__icon(name='fa-solid fa-circle-info' size='18px')
|
||||
.banner__body
|
||||
| Выберите провайдера входящих платежей по умолчанию для вашего кооператива.
|
||||
| Этот провайдер будет использоваться для создания и обработки платежей пайщиков.
|
||||
|
||||
PaymentProviderForm(
|
||||
:loading='saving'
|
||||
@submit='onSubmit'
|
||||
@success='onSuccess'
|
||||
@error='onError'
|
||||
)
|
||||
q-card(flat)
|
||||
q-card-section
|
||||
PaymentProviderForm(
|
||||
:loading='saving'
|
||||
@submit='onSubmit'
|
||||
@success='onSuccess'
|
||||
@error='onError'
|
||||
)
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
@@ -43,31 +45,14 @@ const onError = (error: Error) => {
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.page-shell {
|
||||
.payment-provider-page {
|
||||
width: 100%;
|
||||
padding: 24px 12px 32px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.hero-card {
|
||||
border-radius: 18px;
|
||||
padding: 18px 20px;
|
||||
}
|
||||
|
||||
.hero-title {
|
||||
font-size: 22px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.hero-subtitle {
|
||||
line-height: 1.55;
|
||||
max-width: 820px;
|
||||
}
|
||||
|
||||
.surface-card {
|
||||
border-radius: 16px;
|
||||
padding: 16px 18px;
|
||||
gap: var(--p-5, 20px);
|
||||
padding: var(--p-6, 24px);
|
||||
@media (max-width: 768px) {
|
||||
padding: var(--p-4, 16px);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
+23
-38
@@ -1,19 +1,21 @@
|
||||
<template lang="pug">
|
||||
div.page-shell
|
||||
q-card.hero-card(flat)
|
||||
.hero-title Стартовые страницы
|
||||
.hero-subtitle
|
||||
| Настройте рабочие столы и страницы, которые будут открываться по умолчанию
|
||||
| для новых пользователей при входе на сайт. Единый стиль помогает быстрее
|
||||
| понять, что важно сделать в первую очередь.
|
||||
.settings-page
|
||||
//- Заголовок страницы не дублируем — он уже показан крошкой в шапке.
|
||||
//- Пояснение размещаем на канон-поверхности (.banner), а не на голом фоне.
|
||||
.banner
|
||||
q-icon.banner__icon(name='fa-solid fa-circle-info' size='18px')
|
||||
.banner__body
|
||||
| Настройте рабочие столы и страницы, которые будут открываться по
|
||||
| умолчанию для новых пользователей при входе на сайт.
|
||||
|
||||
q-card.surface-card(flat)
|
||||
DefaultPagesForm(
|
||||
:loading='saving'
|
||||
@submit='onSubmit'
|
||||
@success='onSuccess'
|
||||
@error='onError'
|
||||
)
|
||||
q-card(flat)
|
||||
q-card-section
|
||||
DefaultPagesForm(
|
||||
:loading='saving'
|
||||
@submit='onSubmit'
|
||||
@success='onSuccess'
|
||||
@error='onError'
|
||||
)
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
@@ -45,31 +47,14 @@ const onError = (error: Error) => {
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.page-shell {
|
||||
width: 100%;
|
||||
padding: 24px 12px 32px;
|
||||
// Канон-отступы страницы (как на прочих столах).
|
||||
.settings-page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.hero-card {
|
||||
border-radius: 18px;
|
||||
padding: 18px 20px;
|
||||
}
|
||||
|
||||
.hero-title {
|
||||
font-size: 22px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.hero-subtitle {
|
||||
line-height: 1.55;
|
||||
max-width: 820px;
|
||||
}
|
||||
|
||||
.surface-card {
|
||||
border-radius: 16px;
|
||||
padding: 16px 18px;
|
||||
gap: var(--p-5, 20px);
|
||||
padding: var(--p-6, 24px);
|
||||
@media (max-width: 768px) {
|
||||
padding: var(--p-4, 16px);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import { client } from 'src/shared/api/client';
|
||||
import { Queries, Mutations } from '@coopenomics/sdk';
|
||||
import type { ISecretaryRoom, ICreateSecretaryRoomInput } from '../model/types';
|
||||
|
||||
async function listSecretaryRooms(): Promise<ISecretaryRoom[]> {
|
||||
const { [Queries.ChatCoop.ListSecretaryRooms.name]: rows } = await client.Query(
|
||||
Queries.ChatCoop.ListSecretaryRooms.query,
|
||||
{},
|
||||
);
|
||||
return rows ?? [];
|
||||
}
|
||||
|
||||
async function createSecretaryRoom(data: ICreateSecretaryRoomInput): Promise<ISecretaryRoom> {
|
||||
const { [Mutations.ChatCoop.CreateSecretaryRoom.name]: row } = await client.Mutation(
|
||||
Mutations.ChatCoop.CreateSecretaryRoom.mutation,
|
||||
{ variables: { data } },
|
||||
);
|
||||
return row;
|
||||
}
|
||||
|
||||
async function removeSecretaryRoom(matrixRoomId: string): Promise<string> {
|
||||
const { [Mutations.ChatCoop.RemoveSecretaryRoom.name]: result } = await client.Mutation(
|
||||
Mutations.ChatCoop.RemoveSecretaryRoom.mutation,
|
||||
{ variables: { data: { matrixRoomId } } },
|
||||
);
|
||||
return result;
|
||||
}
|
||||
|
||||
export const api = {
|
||||
listSecretaryRooms,
|
||||
createSecretaryRoom,
|
||||
removeSecretaryRoom,
|
||||
};
|
||||
@@ -0,0 +1,2 @@
|
||||
export { useSecretaryRoomStore } from './model';
|
||||
export * from './model/types';
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './store';
|
||||
export * from './types';
|
||||
@@ -0,0 +1,78 @@
|
||||
import { defineStore } from 'pinia';
|
||||
import { ref, Ref } from 'vue';
|
||||
import { api } from '../api';
|
||||
import type { ISecretaryRoom, ICreateSecretaryRoomInput } from './types';
|
||||
|
||||
const namespace = 'secretaryRoomStore';
|
||||
|
||||
interface ISecretaryRoomStore {
|
||||
rooms: Ref<ISecretaryRoom[]>;
|
||||
isLoading: Ref<boolean>;
|
||||
isMutating: Ref<boolean>;
|
||||
error: Ref<string | null>;
|
||||
loadRooms: () => Promise<ISecretaryRoom[]>;
|
||||
createRoom: (data: ICreateSecretaryRoomInput) => Promise<ISecretaryRoom>;
|
||||
removeRoom: (matrixRoomId: string) => Promise<void>;
|
||||
clearError: () => void;
|
||||
}
|
||||
|
||||
export const useSecretaryRoomStore = defineStore(namespace, (): ISecretaryRoomStore => {
|
||||
const rooms = ref<ISecretaryRoom[]>([]);
|
||||
const isLoading = ref(false);
|
||||
const isMutating = ref(false);
|
||||
const error = ref<string | null>(null);
|
||||
|
||||
const loadRooms = async (): Promise<ISecretaryRoom[]> => {
|
||||
isLoading.value = true;
|
||||
error.value = null;
|
||||
try {
|
||||
const result = await api.listSecretaryRooms();
|
||||
rooms.value = result;
|
||||
return result;
|
||||
} catch (err) {
|
||||
console.error('Failed to load secretary rooms:', err);
|
||||
error.value = 'Не удалось загрузить список комнат. Попробуйте обновить страницу.';
|
||||
return [];
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const createRoom = async (data: ICreateSecretaryRoomInput): Promise<ISecretaryRoom> => {
|
||||
isMutating.value = true;
|
||||
error.value = null;
|
||||
try {
|
||||
const room = await api.createSecretaryRoom(data);
|
||||
await loadRooms();
|
||||
return room;
|
||||
} finally {
|
||||
isMutating.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const removeRoom = async (matrixRoomId: string): Promise<void> => {
|
||||
isMutating.value = true;
|
||||
error.value = null;
|
||||
try {
|
||||
await api.removeSecretaryRoom(matrixRoomId);
|
||||
await loadRooms();
|
||||
} finally {
|
||||
isMutating.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const clearError = () => {
|
||||
error.value = null;
|
||||
};
|
||||
|
||||
return {
|
||||
rooms,
|
||||
isLoading,
|
||||
isMutating,
|
||||
error,
|
||||
loadRooms,
|
||||
createRoom,
|
||||
removeRoom,
|
||||
clearError,
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,8 @@
|
||||
import type { Queries, Mutations } from '@coopenomics/sdk';
|
||||
|
||||
/** Комната из реестра ChatCoop (системная / проектная / секретаря) */
|
||||
export type ISecretaryRoom =
|
||||
Queries.ChatCoop.ListSecretaryRooms.IOutput[typeof Queries.ChatCoop.ListSecretaryRooms.name][number];
|
||||
|
||||
/** Вход для создания комнаты секретаря */
|
||||
export type ICreateSecretaryRoomInput = Mutations.ChatCoop.CreateSecretaryRoom.IInput['data'];
|
||||
@@ -1,3 +1,4 @@
|
||||
export * as ChatCoopCalendar from './ChatCoopCalendar';
|
||||
export * as ChatCoopChat from './ChatCoopChat';
|
||||
export * as SecretaryRoom from './SecretaryRoom';
|
||||
export * as Transcription from './Transcription';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { markRaw } from 'vue';
|
||||
import { CalendarPage, ChatCoopPage, MobileClientPage, TranscriptionsPage, TranscriptionDetailPage } from './pages';
|
||||
import { CalendarPage, ChatCoopPage, MobileClientPage, SecretaryRoomsPage, TranscriptionsPage, TranscriptionDetailPage } from './pages';
|
||||
import { agreementsBase } from 'src/shared/lib/consts/workspaces';
|
||||
import type { IWorkspaceConfig } from 'src/shared/lib/types/workspace';
|
||||
|
||||
@@ -73,6 +73,19 @@ export default async function (): Promise<IWorkspaceConfig[]> {
|
||||
},
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
path: 'secretary-rooms',
|
||||
name: 'chatcoop-secretary-rooms',
|
||||
component: markRaw(SecretaryRoomsPage),
|
||||
meta: {
|
||||
title: 'Комнаты секретаря',
|
||||
icon: 'fa-solid fa-user-shield',
|
||||
roles: ['chairman', 'member'],
|
||||
agreements: agreementsBase,
|
||||
requiresAuth: true,
|
||||
},
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
path: 'transcriptions/:id',
|
||||
name: 'chatcoop-transcription-detail',
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export * from './ui'
|
||||
+282
@@ -0,0 +1,282 @@
|
||||
<template lang="pug">
|
||||
q-page.secretary-rooms-page(padding)
|
||||
header.sr-head
|
||||
div.sr-head__text
|
||||
h1.sr-head__title Комнаты секретаря
|
||||
p.sr-head__subtitle
|
||||
| Создавайте комнаты для звонков с секретарём — он подключается сразу и ведёт транскрипцию.
|
||||
| Системные и проектные комнаты показаны для справки и не удаляются.
|
||||
.sr-head__actions
|
||||
q-btn(
|
||||
unelevated
|
||||
no-caps
|
||||
color="primary"
|
||||
icon="fa-solid fa-plus"
|
||||
label="Создать комнату"
|
||||
@click="openCreateDialog"
|
||||
)
|
||||
q-btn.sr-head__refresh(
|
||||
flat
|
||||
round
|
||||
dense
|
||||
icon="fa-solid fa-rotate-right"
|
||||
@click="handleRefresh"
|
||||
:loading="store.isLoading"
|
||||
aria-label="Обновить список"
|
||||
)
|
||||
q-tooltip Обновить
|
||||
|
||||
q-banner.sr-error(v-if="store.error" dense rounded class="bg-red-1 text-red-9 q-mb-md")
|
||||
| {{ store.error }}
|
||||
|
||||
q-table(
|
||||
flat
|
||||
bordered
|
||||
:rows="store.rooms"
|
||||
:columns="columns"
|
||||
row-key="matrixRoomId"
|
||||
:loading="store.isLoading"
|
||||
:rows-per-page-options="[0]"
|
||||
hide-pagination
|
||||
no-data-label="Комнат пока нет"
|
||||
)
|
||||
template(#body-cell-displayLabel="props")
|
||||
q-td(:props="props")
|
||||
.sr-name {{ props.row.displayLabel }}
|
||||
.sr-room-id {{ props.row.matrixRoomId }}
|
||||
template(#body-cell-kind="props")
|
||||
q-td(:props="props")
|
||||
q-badge(:color="kindColor(props.row.kind)" :label="kindLabel(props.row.kind)" outline)
|
||||
template(#body-cell-secretary="props")
|
||||
q-td(:props="props")
|
||||
q-icon(
|
||||
:name="props.row.secretaryInRoom ? 'fa-solid fa-circle-check' : 'fa-solid fa-circle-minus'"
|
||||
:color="props.row.secretaryInRoom ? 'positive' : 'grey-5'"
|
||||
size="18px"
|
||||
)
|
||||
q-tooltip {{ props.row.secretaryInRoom ? 'Секретарь в комнате' : 'Секретаря нет' }}
|
||||
q-icon.q-ml-sm(
|
||||
v-if="props.row.encrypted"
|
||||
name="fa-solid fa-lock"
|
||||
color="orange-8"
|
||||
size="16px"
|
||||
)
|
||||
q-tooltip Зашифрованная комната — транскрипция недоступна
|
||||
template(#body-cell-actions="props")
|
||||
q-td(:props="props" align="right")
|
||||
q-btn(
|
||||
v-if="props.row.editable"
|
||||
flat
|
||||
dense
|
||||
no-caps
|
||||
color="negative"
|
||||
icon="fa-solid fa-trash"
|
||||
label="Удалить"
|
||||
@click="confirmRemove(props.row)"
|
||||
)
|
||||
span.text-grey-5(v-else) —
|
||||
|
||||
//- Диалог создания комнаты
|
||||
q-dialog(v-model="createDialog")
|
||||
q-card.sr-dialog
|
||||
q-card-section
|
||||
.text-h6 Новая комната секретаря
|
||||
q-card-section.q-pt-none
|
||||
q-input(
|
||||
v-model="form.displayName"
|
||||
label="Название комнаты"
|
||||
autofocus
|
||||
:rules="[(v) => !!v && v.trim().length > 0 || 'Введите название']"
|
||||
maxlength="240"
|
||||
)
|
||||
.sr-type.q-mt-md
|
||||
.text-subtitle2.q-mb-xs Тип комнаты
|
||||
q-option-group(
|
||||
v-model="form.isPublic"
|
||||
:options="typeOptions"
|
||||
color="primary"
|
||||
type="radio"
|
||||
)
|
||||
.text-caption.text-grey-7.q-mt-xs
|
||||
| {{ form.isPublic ? 'Публичная: войти может любой пайщик.' : 'Приватная: участников приглашаете вы (в клиенте мессенджера).' }}
|
||||
q-card-actions(align="right")
|
||||
q-btn(flat no-caps label="Отмена" v-close-popup :disable="store.isMutating")
|
||||
q-btn(
|
||||
unelevated
|
||||
no-caps
|
||||
color="primary"
|
||||
label="Создать"
|
||||
:loading="store.isMutating"
|
||||
@click="submitCreate"
|
||||
)
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { useQuasar } from 'quasar';
|
||||
import { useSecretaryRoomStore } from '../../../entities/SecretaryRoom';
|
||||
import type { ISecretaryRoom } from '../../../entities/SecretaryRoom';
|
||||
|
||||
const $q = useQuasar();
|
||||
const store = useSecretaryRoomStore();
|
||||
|
||||
const createDialog = ref(false);
|
||||
const form = ref<{ displayName: string; isPublic: boolean }>({
|
||||
displayName: '',
|
||||
isPublic: false,
|
||||
});
|
||||
|
||||
const typeOptions = [
|
||||
{ label: 'Приватная (по приглашению)', value: false },
|
||||
{ label: 'Публичная (открытый вход)', value: true },
|
||||
];
|
||||
|
||||
const columns = [
|
||||
{
|
||||
name: 'displayLabel',
|
||||
label: 'Комната',
|
||||
field: 'displayLabel',
|
||||
align: 'left' as const,
|
||||
style: 'min-width: 320px;',
|
||||
},
|
||||
{ name: 'kind', label: 'Тип', field: 'kind', align: 'left' as const },
|
||||
{ name: 'secretary', label: 'Секретарь', field: 'secretaryInRoom', align: 'left' as const },
|
||||
{ name: 'actions', label: '', field: 'actions', align: 'right' as const },
|
||||
];
|
||||
|
||||
function kindLabel(kind: ISecretaryRoom['kind']): string {
|
||||
switch (kind) {
|
||||
case 'MEMBERS':
|
||||
return 'Пайщики';
|
||||
case 'COUNCIL':
|
||||
return 'Совет';
|
||||
case 'CAPITAL_PROJECT':
|
||||
return 'Проект';
|
||||
case 'SECRETARY':
|
||||
return 'Секретарь';
|
||||
default:
|
||||
return String(kind);
|
||||
}
|
||||
}
|
||||
|
||||
function kindColor(kind: ISecretaryRoom['kind']): string {
|
||||
switch (kind) {
|
||||
case 'SECRETARY':
|
||||
return 'primary';
|
||||
case 'CAPITAL_PROJECT':
|
||||
return 'teal';
|
||||
default:
|
||||
return 'grey-7';
|
||||
}
|
||||
}
|
||||
|
||||
function openCreateDialog(): void {
|
||||
form.value = { displayName: '', isPublic: false };
|
||||
store.clearError();
|
||||
createDialog.value = true;
|
||||
}
|
||||
|
||||
async function submitCreate(): Promise<void> {
|
||||
const name = form.value.displayName.trim();
|
||||
if (name.length === 0) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await store.createRoom({ displayName: name, isPublic: form.value.isPublic });
|
||||
createDialog.value = false;
|
||||
$q.notify({ type: 'positive', message: 'Комната создана, секретарь подключён' });
|
||||
} catch (err) {
|
||||
$q.notify({ type: 'negative', message: extractError(err) });
|
||||
}
|
||||
}
|
||||
|
||||
function confirmRemove(room: ISecretaryRoom): void {
|
||||
$q.dialog({
|
||||
title: 'Удалить комнату секретаря?',
|
||||
message: `Секретарь выйдет из «${room.displayLabel}», комната перестанет транскрибироваться и синхронизироваться.`,
|
||||
cancel: { label: 'Отмена', flat: true, noCaps: true },
|
||||
ok: { label: 'Удалить', color: 'negative', noCaps: true, unelevated: true },
|
||||
persistent: true,
|
||||
}).onOk(async () => {
|
||||
try {
|
||||
await store.removeRoom(room.matrixRoomId);
|
||||
$q.notify({ type: 'positive', message: 'Комната удалена' });
|
||||
} catch (err) {
|
||||
$q.notify({ type: 'negative', message: extractError(err) });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function extractError(err: unknown): string {
|
||||
if (err instanceof Error) {
|
||||
return err.message;
|
||||
}
|
||||
return 'Не удалось выполнить операцию';
|
||||
}
|
||||
|
||||
async function handleRefresh(): Promise<void> {
|
||||
await store.loadRooms();
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await store.loadRooms();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.sr-head {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.sr-head__title {
|
||||
margin: 0;
|
||||
font-size: 1.35rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: -0.02em;
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
.sr-head__subtitle {
|
||||
margin: 6px 0 0;
|
||||
font-size: 0.8125rem;
|
||||
line-height: 1.45;
|
||||
color: rgba(0, 0, 0, 0.55);
|
||||
max-width: 48rem;
|
||||
}
|
||||
|
||||
.body--dark .sr-head__subtitle {
|
||||
color: rgba(255, 255, 255, 0.55);
|
||||
}
|
||||
|
||||
.sr-head__actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.sr-name {
|
||||
font-weight: 500;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.sr-room-id {
|
||||
margin-top: 2px;
|
||||
font-size: 0.75rem;
|
||||
color: rgba(0, 0, 0, 0.45);
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.body--dark .sr-room-id {
|
||||
color: rgba(255, 255, 255, 0.45);
|
||||
}
|
||||
|
||||
.sr-dialog {
|
||||
width: 460px;
|
||||
max-width: 90vw;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1 @@
|
||||
export { default as SecretaryRoomsPage } from './SecretaryRoomsPage.vue'
|
||||
@@ -1,5 +1,6 @@
|
||||
export { CalendarPage } from './CalendarPage';
|
||||
export { ChatCoopPage } from './ChatCoopPage';
|
||||
export { MobileClientPage } from './MobileClientPage';
|
||||
export { SecretaryRoomsPage } from './SecretaryRoomsPage';
|
||||
export { TranscriptionsPage } from './TranscriptionsPage';
|
||||
export { TranscriptionDetailPage } from './TranscriptionDetailPage';
|
||||
|
||||
@@ -12,6 +12,14 @@ import { PowerupLogItem } from '../../../widgets/PowerupLogItem'
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.logs-page {
|
||||
height: 100%;
|
||||
padding: var(--p-6, 24px);
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.logs-page {
|
||||
padding: var(--p-4, 16px);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,62 +1,73 @@
|
||||
<template lang="pug">
|
||||
div.monitor-page
|
||||
// Ресурсные карточки в ряд
|
||||
.resources-row
|
||||
.resources-container
|
||||
.resource-col
|
||||
CpuResourceWidget
|
||||
.resource-col
|
||||
NetResourceWidget
|
||||
.resource-col
|
||||
RamResourceWidget
|
||||
.monitor-page
|
||||
//- Предупреждение об исчерпании квот — баннером на всю ширину сверху.
|
||||
BaseBanner.monitor-page__alert(variant="warn")
|
||||
template(#icon)
|
||||
q-icon(name="warning", size="20px")
|
||||
| Не допускайте исчерпание квот ресурсов — это заблокирует обработку новых документов в кооперативе.
|
||||
|
||||
// Кошелек AXON
|
||||
div(style="margin-top: 50px;")
|
||||
.row.justify-center
|
||||
.col-xs-12.col-sm-6.col-md-5.q-pa-md
|
||||
AxonWalletCard
|
||||
.col-xs-12.col-sm-6.col-md-5.q-pa-md
|
||||
ResourceInfoWidget
|
||||
//- Верхний ряд: слева (2/3) «как это работает», справа (1/3) кошелёк AXON.
|
||||
//- На узком экране перестраивается в одну колонку: сначала описание, затем кошелёк.
|
||||
.monitor-page__top
|
||||
.monitor-page__info
|
||||
ResourceInfoWidget
|
||||
.monitor-page__wallet
|
||||
AxonWalletCard
|
||||
|
||||
//- Ресурсы в одну строку (CPU / NET / RAM); на узком экране — в колонку.
|
||||
.monitor-page__resources
|
||||
CpuResourceWidget
|
||||
NetResourceWidget
|
||||
RamResourceWidget
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.monitor-page {
|
||||
padding: 2rem;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.resources-row {
|
||||
.resources-container {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
gap: 2rem;
|
||||
flex-wrap: wrap;
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.resource-col {
|
||||
flex: 0 0 auto;
|
||||
max-width: 320px;
|
||||
width: 100%;
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { BaseBanner } from 'src/shared/ui/base/BaseBanner'
|
||||
import CpuResourceWidget from '../../../widgets/CpuResourceWidget'
|
||||
import NetResourceWidget from '../../../widgets/NetResourceWidget'
|
||||
import RamResourceWidget from '../../../widgets/RamResourceWidget'
|
||||
import ResourceInfoWidget from '../../../widgets/ResourceInfoWidget'
|
||||
import AxonWalletCard from '../../../widgets/AxonWalletCard'
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.monitor-page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--p-5, 20px);
|
||||
padding: var(--p-6, 24px);
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.monitor-page__alert {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.monitor-page__top {
|
||||
display: grid;
|
||||
grid-template-columns: 2fr 1fr;
|
||||
gap: var(--p-5, 20px);
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.monitor-page__resources {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: var(--p-5, 20px);
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
.monitor-page__top,
|
||||
.monitor-page__resources {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.monitor-page {
|
||||
padding: var(--p-4, 16px);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
.settings-page
|
||||
|
||||
.settings-content(v-if='extension')
|
||||
.settings-container.q-pa-md
|
||||
.settings-container
|
||||
ExtensionSettings(
|
||||
:schema='extension.schema',
|
||||
:config='extension.config',
|
||||
@@ -112,7 +112,7 @@ const saveSettings = async () => {
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.settings-page {
|
||||
padding: 1rem;
|
||||
padding: var(--p-6, 24px);
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
@@ -123,4 +123,10 @@ const saveSettings = async () => {
|
||||
margin: 0 auto;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.settings-page {
|
||||
padding: var(--p-4, 16px);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
/* Канон-стиль ресурсной карточки монитора (CPU / NET / RAM).
|
||||
Общий для трёх виджетов — каждый переопределяет лишь акцент иконки.
|
||||
Плоская surface-карточка вместо «переворачивающейся» 3D-карты:
|
||||
детали раскрываются inline через q-slide-transition, ничего не прыгает. */
|
||||
.resource-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--p-4, 16px);
|
||||
background: var(--p-surface);
|
||||
border: 1px solid var(--p-line);
|
||||
border-radius: var(--p-r-lg, 16px);
|
||||
padding: var(--p-5, 20px);
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.resource-card__head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--p-3, 12px);
|
||||
}
|
||||
|
||||
.resource-card__icon {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
flex: 0 0 40px;
|
||||
border-radius: var(--p-r-md, 12px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
i {
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.resource-card__title {
|
||||
font-size: var(--p-fs-h3, 15px);
|
||||
font-weight: 600;
|
||||
color: var(--p-ink);
|
||||
}
|
||||
|
||||
.resource-card__gauge {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: var(--p-2, 8px) 0;
|
||||
}
|
||||
|
||||
.gauge-inner {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
.gauge-value {
|
||||
font-size: 1.1rem;
|
||||
font-weight: 700;
|
||||
line-height: 1.2;
|
||||
color: var(--p-ink);
|
||||
}
|
||||
|
||||
.gauge-label {
|
||||
font-size: var(--p-fs-meta, 12px);
|
||||
color: var(--p-ink-3);
|
||||
margin-top: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.resource-card__note {
|
||||
background: var(--p-surface-2);
|
||||
border-radius: var(--p-r-md, 12px);
|
||||
padding: var(--p-3, 12px) var(--p-4, 16px);
|
||||
color: var(--p-ink-2);
|
||||
font-size: var(--p-fs-body-sm, 13px);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.resource-card__toggle {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: var(--p-1, 4px);
|
||||
align-self: center;
|
||||
margin-top: auto;
|
||||
padding: var(--p-1, 4px) var(--p-2, 8px);
|
||||
background: none;
|
||||
border: 0;
|
||||
cursor: pointer;
|
||||
color: var(--p-primary);
|
||||
font-size: var(--p-fs-body-sm, 13px);
|
||||
font-weight: 500;
|
||||
border-radius: var(--p-r-sm, 8px);
|
||||
transition: background var(--p-dur-fast, 120ms) var(--p-ease-standard);
|
||||
|
||||
&:hover {
|
||||
background: var(--p-primary-soft);
|
||||
}
|
||||
}
|
||||
|
||||
.resource-card__details {
|
||||
border-top: 1px solid var(--p-line);
|
||||
padding-top: var(--p-2, 8px);
|
||||
|
||||
.detail-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: var(--p-3, 12px);
|
||||
padding: var(--p-2, 8px) 0;
|
||||
border-bottom: 1px solid var(--p-line);
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
|
||||
.detail-label {
|
||||
font-size: var(--p-fs-body-sm, 13px);
|
||||
color: var(--p-ink-2);
|
||||
}
|
||||
|
||||
.detail-value {
|
||||
font-size: var(--p-fs-mono, 13px);
|
||||
font-weight: 600;
|
||||
color: var(--p-ink);
|
||||
font-family: var(--p-mono);
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
+82
-80
@@ -1,34 +1,29 @@
|
||||
<template lang="pug">
|
||||
div.axon-wallet-card
|
||||
.widget-header
|
||||
.widget-icon
|
||||
.axon-wallet-card
|
||||
.axon-wallet-card__head
|
||||
.axon-wallet-card__icon
|
||||
i.fas.fa-wallet
|
||||
.widget-title Кошелек AXON
|
||||
.axon-wallet-card__title Кошелёк AXON
|
||||
|
||||
.wallet-balance
|
||||
.balance-value {{ formattedBalance }}
|
||||
.balance-label Доступно
|
||||
.axon-wallet-card__balance
|
||||
.axon-wallet-card__balance-value {{ formattedBalance }}
|
||||
.axon-wallet-card__balance-label Доступно
|
||||
|
||||
.axon-details
|
||||
.axon-wallet-card__details
|
||||
.detail-row
|
||||
.detail-label Минимальных квот:
|
||||
.detail-value {{ minQuotasDays }} {{ minQuotasDaysText }}
|
||||
span.detail-label Минимальных квот
|
||||
span.detail-value {{ minQuotasDays }} {{ minQuotasDaysText }}
|
||||
.detail-row
|
||||
.detail-label Новых пайщиков:
|
||||
.detail-value {{ maxNewMembers }} {{ maxNewMembersText }}
|
||||
span.detail-label Новых пайщиков
|
||||
span.detail-value {{ maxNewMembers }} {{ maxNewMembersText }}
|
||||
|
||||
ColorCard(color='purple')
|
||||
.axon-wallet-card__note
|
||||
| AXON используется для аренды вычислительных ресурсов (минимум 5 AXON/день) и регистрации пайщиков (1 AXON/аккаунт). Курс: 1 AXON = 10 RUB.
|
||||
|
||||
q-btn.wallet-btn(
|
||||
flat
|
||||
no-caps
|
||||
size="md"
|
||||
color="primary"
|
||||
icon="open_in_new"
|
||||
label="Пополнить у оператора"
|
||||
@click="goToReplenishment"
|
||||
)
|
||||
BaseButton.axon-wallet-card__action(variant="primary", size="sm", block, @click="goToReplenishment")
|
||||
template(#icon-left)
|
||||
q-icon(name="open_in_new", size="16px")
|
||||
| Пополнить у оператора
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
@@ -36,7 +31,7 @@ import { computed } from 'vue';
|
||||
import { useSystemStore } from 'src/entities/System/model';
|
||||
import { formatAsset2Digits } from 'src/shared/lib/utils/formatAsset2Digits';
|
||||
import { pluralizeDays, pluralizeAccounts } from 'src/shared/lib/utils';
|
||||
import { ColorCard } from 'src/shared/ui';
|
||||
import { BaseButton } from 'src/shared/ui/base/BaseButton';
|
||||
|
||||
const systemStore = useSystemStore();
|
||||
|
||||
@@ -79,95 +74,102 @@ const goToReplenishment = () => {
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
/* Канон-карточка кошелька AXON: плоская surface, без градиента/теней. */
|
||||
.axon-wallet-card {
|
||||
border-radius: 16px;
|
||||
padding: 1.5rem;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
|
||||
border: 1px solid #e0e6ed;
|
||||
transition: all 0.3s ease;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--p-4, 16px);
|
||||
background: var(--p-surface);
|
||||
border: 1px solid var(--p-line);
|
||||
border-radius: var(--p-r-lg, 16px);
|
||||
padding: var(--p-5, 20px);
|
||||
height: 100%;
|
||||
min-width: 320px;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
}
|
||||
|
||||
.widget-header {
|
||||
.axon-wallet-card__head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 1.5rem;
|
||||
gap: var(--p-3, 12px);
|
||||
}
|
||||
|
||||
.widget-icon {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 12px;
|
||||
background: linear-gradient(135deg, #8e44ad 0%, #9b59b6 100%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 1rem;
|
||||
.axon-wallet-card__icon {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
flex: 0 0 40px;
|
||||
border-radius: var(--p-r-md, 12px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: var(--prog-generator-soft);
|
||||
color: var(--prog-generator);
|
||||
|
||||
i {
|
||||
color: white;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
}
|
||||
|
||||
.widget-title {
|
||||
i {
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.wallet-balance {
|
||||
text-align: center;
|
||||
margin-bottom: 1rem;
|
||||
.axon-wallet-card__title {
|
||||
font-size: var(--p-fs-h3, 15px);
|
||||
font-weight: 600;
|
||||
color: var(--p-ink);
|
||||
}
|
||||
|
||||
.balance-value {
|
||||
font-size: 2rem;
|
||||
.axon-wallet-card__balance {
|
||||
text-align: center;
|
||||
padding: var(--p-2, 8px) 0;
|
||||
|
||||
.axon-wallet-card__balance-value {
|
||||
font-size: 1.9rem;
|
||||
font-weight: 700;
|
||||
line-height: 1.2;
|
||||
margin-bottom: 0.5rem;
|
||||
color: var(--p-ink);
|
||||
}
|
||||
|
||||
.balance-label {
|
||||
font-size: 0.8rem;
|
||||
opacity: 0.7;
|
||||
.axon-wallet-card__balance-label {
|
||||
font-size: var(--p-fs-meta, 12px);
|
||||
color: var(--p-ink-3);
|
||||
margin-top: var(--p-1, 4px);
|
||||
}
|
||||
}
|
||||
|
||||
.axon-details {
|
||||
margin-bottom: 1rem;
|
||||
|
||||
.axon-wallet-card__details {
|
||||
.detail-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0.5rem 0;
|
||||
border-bottom: 1px solid #f1f3f4;
|
||||
gap: var(--p-3, 12px);
|
||||
padding: var(--p-2, 8px) 0;
|
||||
border-bottom: 1px solid var(--p-line);
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
|
||||
.detail-label {
|
||||
font-size: 0.85rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
.detail-label {
|
||||
font-size: var(--p-fs-body-sm, 13px);
|
||||
color: var(--p-ink-2);
|
||||
}
|
||||
|
||||
.detail-value {
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
font-family: 'SF Mono', 'Monaco', 'Inconsolata', monospace;
|
||||
}
|
||||
.detail-value {
|
||||
font-size: var(--p-fs-mono, 13px);
|
||||
font-weight: 600;
|
||||
color: var(--p-ink);
|
||||
font-family: var(--p-mono);
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
|
||||
.wallet-btn {
|
||||
margin-top: 1rem;
|
||||
width: 100%;
|
||||
.axon-wallet-card__note {
|
||||
background: var(--p-surface-2);
|
||||
border-radius: var(--p-r-md, 12px);
|
||||
padding: var(--p-3, 12px) var(--p-4, 16px);
|
||||
color: var(--p-ink-2);
|
||||
font-size: var(--p-fs-body-sm, 13px);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.axon-wallet-card__action {
|
||||
margin-top: auto;
|
||||
}
|
||||
</style>
|
||||
|
||||
+51
-222
@@ -1,231 +1,49 @@
|
||||
<template lang="pug">
|
||||
div.cpu-resource-widget
|
||||
.card-flipper(:class="{ flipped: showDetails }")
|
||||
.card-front.front
|
||||
.card-content
|
||||
.widget-header
|
||||
.widget-icon
|
||||
i.fas.fa-microchip
|
||||
.widget-title Время процессора (CPU)
|
||||
.resource-card
|
||||
.resource-card__head
|
||||
.resource-card__icon
|
||||
i.fas.fa-microchip
|
||||
.resource-card__title Время процессора (CPU)
|
||||
|
||||
.widget-content
|
||||
q-circular-progress(
|
||||
:value="usagePercent"
|
||||
size="180px"
|
||||
:thickness="0.35"
|
||||
color="primary"
|
||||
track-color="grey-3"
|
||||
show-value
|
||||
:class="progressClass"
|
||||
)
|
||||
.progress-content
|
||||
.usage-value {{ usagePercent.toFixed(2) }}%
|
||||
.usage-label Использовано
|
||||
.resource-card__gauge
|
||||
q-circular-progress(
|
||||
:value="usagePercent"
|
||||
size="148px"
|
||||
:thickness="0.14"
|
||||
:color="gaugeColor"
|
||||
track-color="grey-3"
|
||||
rounded
|
||||
show-value
|
||||
)
|
||||
.gauge-inner
|
||||
.gauge-value {{ usagePercent.toFixed(2) }}%
|
||||
.gauge-label Использовано
|
||||
|
||||
ColorCard(color='blue')
|
||||
| CPU расходуется для вычислений при обработке пакетов документов.
|
||||
.resource-card__note CPU расходуется для вычислений при обработке пакетов документов.
|
||||
|
||||
q-btn.flip-btn(
|
||||
flat
|
||||
dense
|
||||
no-caps
|
||||
size="sm"
|
||||
label="Подробнее"
|
||||
@click="showDetails = !showDetails"
|
||||
)
|
||||
button.resource-card__toggle(type="button", @click="showDetails = !showDetails")
|
||||
span {{ showDetails ? 'Скрыть детали' : 'Подробнее' }}
|
||||
q-icon(:name="showDetails ? 'expand_less' : 'expand_more'", size="18px")
|
||||
|
||||
.card-back.back
|
||||
.card-content
|
||||
.widget-header
|
||||
.widget-icon
|
||||
i.fas.fa-microchip
|
||||
.widget-title Детали CPU
|
||||
|
||||
.resource-details
|
||||
.detail-row
|
||||
.detail-label Используется:
|
||||
.detail-value {{ formatValue(currentUsed) }} μs
|
||||
.detail-row
|
||||
.detail-label Доступно:
|
||||
.detail-value {{ formatValue(available) }} μs ({{ availablePercent.toFixed(1) }}%)
|
||||
.detail-row
|
||||
.detail-label Максимум:
|
||||
.detail-value {{ formatValue(max) }} μs
|
||||
.detail-row
|
||||
.detail-label Восстановление:
|
||||
.detail-value ~{{ formatTime(estimatedRecovery) }}
|
||||
|
||||
q-btn.flip-btn(
|
||||
flat
|
||||
dense
|
||||
no-caps
|
||||
size="sm"
|
||||
label="Назад"
|
||||
@click="showDetails = !showDetails"
|
||||
)
|
||||
q-slide-transition
|
||||
.resource-card__details(v-show="showDetails")
|
||||
.detail-row
|
||||
span.detail-label Используется
|
||||
span.detail-value {{ formatValue(currentUsed) }} μs
|
||||
.detail-row
|
||||
span.detail-label Доступно
|
||||
span.detail-value {{ formatValue(available) }} μs ({{ availablePercent.toFixed(1) }}%)
|
||||
.detail-row
|
||||
span.detail-label Максимум
|
||||
span.detail-value {{ formatValue(max) }} μs
|
||||
.detail-row
|
||||
span.detail-label Восстановление
|
||||
span.detail-value ~{{ formatTime(estimatedRecovery) }}
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.cpu-resource-widget {
|
||||
border-radius: 16px;
|
||||
padding: 1.5rem;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
|
||||
border: 1px solid #e0e6ed;
|
||||
transition: all 0.3s ease;
|
||||
min-width: 320px;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
}
|
||||
|
||||
.widget-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 1.5rem;
|
||||
|
||||
.widget-icon {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 12px;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 1rem;
|
||||
|
||||
i {
|
||||
color: white;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
}
|
||||
|
||||
.widget-title {
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.widget-content {
|
||||
text-align: center;
|
||||
|
||||
.q-circular-progress {
|
||||
margin: 0 auto 1.5rem;
|
||||
|
||||
&.high-usage {
|
||||
::v-deep(.q-circular-progress__track) {
|
||||
stroke: #f39c12 !important;
|
||||
}
|
||||
}
|
||||
|
||||
&.critical-usage {
|
||||
::v-deep(.q-circular-progress__track) {
|
||||
stroke: #e74c3c !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.progress-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
text-align: center;
|
||||
|
||||
.usage-value {
|
||||
font-size: 1.1rem;
|
||||
font-weight: 700;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.usage-label {
|
||||
font-size: 0.8rem;
|
||||
margin-top: 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.resource-details {
|
||||
.detail-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0.5rem 0;
|
||||
border-bottom: 1px solid #f1f3f4;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.detail-label {
|
||||
font-size: 0.85rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.detail-value {
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
font-family: 'SF Mono', 'Monaco', 'Inconsolata', monospace;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.card-flipper {
|
||||
position: relative;
|
||||
perspective: 1000px;
|
||||
min-height: 450px;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.front,
|
||||
.back {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
backface-visibility: hidden;
|
||||
transition: transform 0.8s ease-in-out;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.front {
|
||||
transform: rotateY(0deg);
|
||||
}
|
||||
|
||||
.back {
|
||||
transform: rotateY(180deg);
|
||||
}
|
||||
|
||||
&.flipped {
|
||||
.front {
|
||||
transform: rotateY(-180deg);
|
||||
}
|
||||
|
||||
.back {
|
||||
transform: rotateY(0deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.card-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.flip-btn {
|
||||
margin-top: auto;
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref } from 'vue'
|
||||
import { useSystemStore } from 'src/entities/System/model'
|
||||
import { ColorCard } from 'src/shared/ui'
|
||||
|
||||
const showDetails = ref(false)
|
||||
|
||||
@@ -265,11 +83,13 @@ const availablePercent = computed(() => {
|
||||
return (available.value / max.value) * 100
|
||||
})
|
||||
|
||||
const progressClass = computed(() => {
|
||||
// Цвет кольца: канон-статусы. Базовый — primary; при перегрузке квоты
|
||||
// переключаемся на предупреждающий и негативный.
|
||||
const gaugeColor = computed(() => {
|
||||
const percent = usagePercent.value
|
||||
if (percent >= 90) return 'critical-usage'
|
||||
if (percent >= 70) return 'high-usage'
|
||||
return ''
|
||||
if (percent >= 90) return 'negative'
|
||||
if (percent >= 70) return 'warning'
|
||||
return 'primary'
|
||||
})
|
||||
|
||||
// Примерная оценка времени восстановления (упрощенная логика)
|
||||
@@ -305,3 +125,12 @@ const formatTime = (seconds: number): string => {
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@use './../../../shared/resource-card.scss';
|
||||
|
||||
.resource-card__icon {
|
||||
background: var(--p-primary-soft);
|
||||
color: var(--p-primary);
|
||||
}
|
||||
</style>
|
||||
|
||||
+51
-222
@@ -1,231 +1,49 @@
|
||||
<template lang="pug">
|
||||
div.net-resource-widget
|
||||
.card-flipper(:class="{ flipped: showDetails }")
|
||||
.card-front.front
|
||||
.card-content
|
||||
.widget-header
|
||||
.widget-icon
|
||||
i.fas.fa-network-wired
|
||||
.widget-title Сетевой трафик (NET)
|
||||
.resource-card
|
||||
.resource-card__head
|
||||
.resource-card__icon
|
||||
i.fas.fa-network-wired
|
||||
.resource-card__title Сетевой трафик (NET)
|
||||
|
||||
.widget-content
|
||||
q-circular-progress(
|
||||
:value="usagePercent"
|
||||
size="180px"
|
||||
:thickness="0.35"
|
||||
color="secondary"
|
||||
track-color="grey-3"
|
||||
show-value
|
||||
:class="progressClass"
|
||||
)
|
||||
.progress-content
|
||||
.usage-value {{ usagePercent.toFixed(2) }}%
|
||||
.usage-label Использовано
|
||||
.resource-card__gauge
|
||||
q-circular-progress(
|
||||
:value="usagePercent"
|
||||
size="148px"
|
||||
:thickness="0.14"
|
||||
:color="gaugeColor"
|
||||
track-color="grey-3"
|
||||
rounded
|
||||
show-value
|
||||
)
|
||||
.gauge-inner
|
||||
.gauge-value {{ usagePercent.toFixed(2) }}%
|
||||
.gauge-label Использовано
|
||||
|
||||
ColorCard(color='green')
|
||||
| NET используется для передачи данных документов между узлами блокчейна.
|
||||
.resource-card__note NET используется для передачи данных документов между узлами блокчейна.
|
||||
|
||||
q-btn.flip-btn(
|
||||
flat
|
||||
dense
|
||||
no-caps
|
||||
size="sm"
|
||||
label="Подробнее"
|
||||
@click="showDetails = !showDetails"
|
||||
)
|
||||
button.resource-card__toggle(type="button", @click="showDetails = !showDetails")
|
||||
span {{ showDetails ? 'Скрыть детали' : 'Подробнее' }}
|
||||
q-icon(:name="showDetails ? 'expand_less' : 'expand_more'", size="18px")
|
||||
|
||||
.card-back.back
|
||||
.card-content
|
||||
.widget-header
|
||||
.widget-icon
|
||||
i.fas.fa-network-wired
|
||||
.widget-title Детали NET
|
||||
|
||||
.resource-details
|
||||
.detail-row
|
||||
.detail-label Используется:
|
||||
.detail-value {{ formatBytes(currentUsed) }}
|
||||
.detail-row
|
||||
.detail-label Доступно:
|
||||
.detail-value {{ formatBytes(available) }} ({{ availablePercent.toFixed(1) }}%)
|
||||
.detail-row
|
||||
.detail-label Максимум:
|
||||
.detail-value {{ formatBytes(max) }}
|
||||
.detail-row
|
||||
.detail-label Восстановление:
|
||||
.detail-value ~{{ formatTime(estimatedRecovery) }}
|
||||
|
||||
q-btn.flip-btn(
|
||||
flat
|
||||
dense
|
||||
no-caps
|
||||
size="sm"
|
||||
label="Назад"
|
||||
@click="showDetails = !showDetails"
|
||||
)
|
||||
q-slide-transition
|
||||
.resource-card__details(v-show="showDetails")
|
||||
.detail-row
|
||||
span.detail-label Используется
|
||||
span.detail-value {{ formatBytes(currentUsed) }}
|
||||
.detail-row
|
||||
span.detail-label Доступно
|
||||
span.detail-value {{ formatBytes(available) }} ({{ availablePercent.toFixed(1) }}%)
|
||||
.detail-row
|
||||
span.detail-label Максимум
|
||||
span.detail-value {{ formatBytes(max) }}
|
||||
.detail-row
|
||||
span.detail-label Восстановление
|
||||
span.detail-value ~{{ formatTime(estimatedRecovery) }}
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.net-resource-widget {
|
||||
border-radius: 16px;
|
||||
padding: 1.5rem;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
|
||||
border: 1px solid #e0e6ed;
|
||||
transition: all 0.3s ease;
|
||||
min-width: 320px;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
}
|
||||
|
||||
.widget-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 1.5rem;
|
||||
|
||||
.widget-icon {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 12px;
|
||||
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 1rem;
|
||||
|
||||
i {
|
||||
color: white;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
}
|
||||
|
||||
.widget-title {
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.widget-content {
|
||||
text-align: center;
|
||||
|
||||
.q-circular-progress {
|
||||
margin: 0 auto 1.5rem;
|
||||
|
||||
&.high-usage {
|
||||
::v-deep(.q-circular-progress__track) {
|
||||
stroke: #f39c12 !important;
|
||||
}
|
||||
}
|
||||
|
||||
&.critical-usage {
|
||||
::v-deep(.q-circular-progress__track) {
|
||||
stroke: #e74c3c !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.progress-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
text-align: center;
|
||||
|
||||
.usage-value {
|
||||
font-size: 1.1rem;
|
||||
font-weight: 700;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.usage-label {
|
||||
font-size: 0.8rem;
|
||||
margin-top: 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.resource-details {
|
||||
.detail-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0.5rem 0;
|
||||
border-bottom: 1px solid #f1f3f4;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.detail-label {
|
||||
font-size: 0.85rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.detail-value {
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
font-family: 'SF Mono', 'Monaco', 'Inconsolata', monospace;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.card-flipper {
|
||||
position: relative;
|
||||
perspective: 1000px;
|
||||
min-height: 450px;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.front,
|
||||
.back {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
backface-visibility: hidden;
|
||||
transition: transform 0.8s ease-in-out;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.front {
|
||||
transform: rotateY(0deg);
|
||||
}
|
||||
|
||||
.back {
|
||||
transform: rotateY(180deg);
|
||||
}
|
||||
|
||||
&.flipped {
|
||||
.front {
|
||||
transform: rotateY(-180deg);
|
||||
}
|
||||
|
||||
.back {
|
||||
transform: rotateY(0deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.card-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.flip-btn {
|
||||
margin-top: auto;
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref } from 'vue'
|
||||
import { useSystemStore } from 'src/entities/System/model'
|
||||
import { ColorCard } from 'src/shared/ui'
|
||||
|
||||
const showDetails = ref(false)
|
||||
|
||||
@@ -265,11 +83,13 @@ const availablePercent = computed(() => {
|
||||
return (available.value / max.value) * 100
|
||||
})
|
||||
|
||||
const progressClass = computed(() => {
|
||||
// Цвет кольца: канон-статусы. Базовый — info; при перегрузке квоты
|
||||
// переключаемся на предупреждающий и негативный.
|
||||
const gaugeColor = computed(() => {
|
||||
const percent = usagePercent.value
|
||||
if (percent >= 90) return 'critical-usage'
|
||||
if (percent >= 70) return 'high-usage'
|
||||
return ''
|
||||
if (percent >= 90) return 'negative'
|
||||
if (percent >= 70) return 'warning'
|
||||
return 'info'
|
||||
})
|
||||
|
||||
// Примерная оценка времени восстановления (упрощенная логика)
|
||||
@@ -306,3 +126,12 @@ const formatTime = (seconds: number): string => {
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@use './../../../shared/resource-card.scss';
|
||||
|
||||
.resource-card__icon {
|
||||
background: var(--p-info-soft);
|
||||
color: var(--p-info);
|
||||
}
|
||||
</style>
|
||||
|
||||
+30
-30
@@ -1,33 +1,30 @@
|
||||
<template lang="pug">
|
||||
div.log-details
|
||||
.log-details
|
||||
.detail-row
|
||||
.detail-label Тип операции:
|
||||
.detail-value
|
||||
q-chip(
|
||||
:color="getTypeColor(log.type)"
|
||||
text-color="white"
|
||||
size="sm"
|
||||
dense
|
||||
) {{ getTypeLabel(log.type) }}
|
||||
span.detail-label Тип операции
|
||||
span.detail-value
|
||||
BaseBadge(:variant="getTypeVariant(log.type)") {{ getTypeLabel(log.type) }}
|
||||
|
||||
.detail-row
|
||||
.detail-label Сумма:
|
||||
.detail-value {{ log.amount }}
|
||||
span.detail-label Сумма
|
||||
span.detail-value {{ log.amount }}
|
||||
|
||||
.detail-row
|
||||
.detail-label RAM:
|
||||
.detail-value {{ formatBytes(log.resources.ram_usage) }} / {{ formatBytes(log.resources.ram_quota) }} ({{ calculateRamPercent(log.resources.ram_usage, log.resources.ram_quota).toFixed(2) }}% использовано)
|
||||
span.detail-label RAM
|
||||
span.detail-value {{ formatBytes(log.resources.ram_usage) }} / {{ formatBytes(log.resources.ram_quota) }} ({{ calculateRamPercent(log.resources.ram_usage, log.resources.ram_quota).toFixed(2) }}% использовано)
|
||||
|
||||
.detail-row
|
||||
.detail-label CPU:
|
||||
.detail-value {{ formatCpuNet(log.resources.cpu_limit) }} ({{ calculateCpuNetPercent(log.resources.cpu_limit).toFixed(2) }}% использовано)
|
||||
span.detail-label CPU
|
||||
span.detail-value {{ formatCpuNet(log.resources.cpu_limit) }} ({{ calculateCpuNetPercent(log.resources.cpu_limit).toFixed(2) }}% использовано)
|
||||
|
||||
.detail-row
|
||||
.detail-label NET:
|
||||
.detail-value {{ formatCpuNet(log.resources.net_limit) }} ({{ calculateCpuNetPercent(log.resources.net_limit).toFixed(2) }}% использовано)
|
||||
span.detail-label NET
|
||||
span.detail-value {{ formatCpuNet(log.resources.net_limit) }} ({{ calculateCpuNetPercent(log.resources.net_limit).toFixed(2) }}% использовано)
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { BaseBadge } from 'src/shared/ui/base/BaseBadge'
|
||||
import type { BaseBadgeVariant } from 'src/shared/ui/base/BaseBadge'
|
||||
|
||||
interface PowerupLog {
|
||||
type: 'daily' | 'now'
|
||||
@@ -52,8 +49,8 @@ const getTypeLabel = (type: string) => {
|
||||
return type === 'daily' ? 'Ежедневное пополнение' : 'Немедленное пополнение'
|
||||
}
|
||||
|
||||
const getTypeColor = (type: string) => {
|
||||
return type === 'daily' ? 'green' : 'blue'
|
||||
const getTypeVariant = (type: string): BaseBadgeVariant => {
|
||||
return type === 'daily' ? 'pos' : 'info'
|
||||
}
|
||||
|
||||
const formatBytes = (value: any) => {
|
||||
@@ -102,23 +99,26 @@ const calculateCpuNetPercent = (resource: any) => {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0.5rem 0;
|
||||
border-bottom: 1px solid rgba(241, 243, 244, 0.3);
|
||||
gap: var(--p-3, 12px);
|
||||
padding: var(--p-2, 8px) 0;
|
||||
border-bottom: 1px solid var(--p-line);
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
|
||||
.detail-label {
|
||||
font-size: 0.85rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
.detail-label {
|
||||
font-size: var(--p-fs-body-sm, 13px);
|
||||
color: var(--p-ink-2);
|
||||
}
|
||||
|
||||
.detail-value {
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
font-family: 'SF Mono', 'Monaco', 'Inconsolata', monospace;
|
||||
}
|
||||
.detail-value {
|
||||
font-size: var(--p-fs-mono, 13px);
|
||||
font-weight: 600;
|
||||
color: var(--p-ink);
|
||||
font-family: var(--p-mono);
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
+51
-222
@@ -1,231 +1,49 @@
|
||||
<template lang="pug">
|
||||
div.ram-resource-widget
|
||||
.card-flipper(:class="{ flipped: showDetails }")
|
||||
.card-front.front
|
||||
.card-content
|
||||
.widget-header
|
||||
.widget-icon
|
||||
i.fas.fa-memory
|
||||
.widget-title Оперативная память (RAM)
|
||||
.resource-card
|
||||
.resource-card__head
|
||||
.resource-card__icon
|
||||
i.fas.fa-memory
|
||||
.resource-card__title Оперативная память (RAM)
|
||||
|
||||
.widget-content
|
||||
q-circular-progress(
|
||||
:value="usagePercent"
|
||||
size="180px"
|
||||
:thickness="0.35"
|
||||
color="positive"
|
||||
track-color="grey-3"
|
||||
show-value
|
||||
:class="progressClass"
|
||||
)
|
||||
.progress-content
|
||||
.usage-value {{ usagePercent.toFixed(2) }}%
|
||||
.usage-label Использовано
|
||||
.resource-card__gauge
|
||||
q-circular-progress(
|
||||
:value="usagePercent"
|
||||
size="148px"
|
||||
:thickness="0.14"
|
||||
:color="gaugeColor"
|
||||
track-color="grey-3"
|
||||
rounded
|
||||
show-value
|
||||
)
|
||||
.gauge-inner
|
||||
.gauge-value {{ usagePercent.toFixed(2) }}%
|
||||
.gauge-label Использовано
|
||||
|
||||
ColorCard(color='orange')
|
||||
| RAM используется для временного хранения данных пайщиков при исполнении смарт-контрактов.
|
||||
.resource-card__note RAM используется для временного хранения данных пайщиков при исполнении смарт-контрактов.
|
||||
|
||||
q-btn.flip-btn(
|
||||
flat
|
||||
dense
|
||||
no-caps
|
||||
size="sm"
|
||||
label="Подробнее"
|
||||
@click="showDetails = !showDetails"
|
||||
)
|
||||
button.resource-card__toggle(type="button", @click="showDetails = !showDetails")
|
||||
span {{ showDetails ? 'Скрыть детали' : 'Подробнее' }}
|
||||
q-icon(:name="showDetails ? 'expand_less' : 'expand_more'", size="18px")
|
||||
|
||||
.card-back.back
|
||||
.card-content
|
||||
.widget-header
|
||||
.widget-icon
|
||||
i.fas.fa-memory
|
||||
.widget-title Детали RAM
|
||||
|
||||
.resource-details
|
||||
.detail-row
|
||||
.detail-label Используется:
|
||||
.detail-value {{ formatBytes(ramUsage) }}
|
||||
.detail-row
|
||||
.detail-label Доступно:
|
||||
.detail-value {{ formatBytes(available) }} ({{ availablePercent.toFixed(1) }}%)
|
||||
.detail-row
|
||||
.detail-label Квота:
|
||||
.detail-value {{ formatBytes(ramQuota) }}
|
||||
.detail-row
|
||||
.detail-label Стоимость:
|
||||
.detail-value {{ formatCost(estimatedCost) }}
|
||||
|
||||
q-btn.flip-btn(
|
||||
flat
|
||||
dense
|
||||
no-caps
|
||||
size="sm"
|
||||
label="Назад"
|
||||
@click="showDetails = !showDetails"
|
||||
)
|
||||
q-slide-transition
|
||||
.resource-card__details(v-show="showDetails")
|
||||
.detail-row
|
||||
span.detail-label Используется
|
||||
span.detail-value {{ formatBytes(ramUsage) }}
|
||||
.detail-row
|
||||
span.detail-label Доступно
|
||||
span.detail-value {{ formatBytes(available) }} ({{ availablePercent.toFixed(1) }}%)
|
||||
.detail-row
|
||||
span.detail-label Квота
|
||||
span.detail-value {{ formatBytes(ramQuota) }}
|
||||
.detail-row
|
||||
span.detail-label Стоимость
|
||||
span.detail-value {{ formatCost(estimatedCost) }}
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.ram-resource-widget {
|
||||
border-radius: 16px;
|
||||
padding: 1.5rem;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
|
||||
border: 1px solid #e0e6ed;
|
||||
transition: all 0.3s ease;
|
||||
min-width: 320px;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
}
|
||||
|
||||
.widget-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 1.5rem;
|
||||
|
||||
.widget-icon {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 12px;
|
||||
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 1rem;
|
||||
|
||||
i {
|
||||
color: white;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
}
|
||||
|
||||
.widget-title {
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.widget-content {
|
||||
text-align: center;
|
||||
|
||||
.q-circular-progress {
|
||||
margin: 0 auto 1.5rem;
|
||||
|
||||
&.high-usage {
|
||||
::v-deep(.q-circular-progress__track) {
|
||||
stroke: #f39c12 !important;
|
||||
}
|
||||
}
|
||||
|
||||
&.critical-usage {
|
||||
::v-deep(.q-circular-progress__track) {
|
||||
stroke: #e74c3c !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.progress-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
text-align: center;
|
||||
|
||||
.usage-value {
|
||||
font-size: 1.1rem;
|
||||
font-weight: 700;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.usage-label {
|
||||
font-size: 0.8rem;
|
||||
margin-top: 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.resource-details {
|
||||
.detail-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0.5rem 0;
|
||||
border-bottom: 1px solid #f1f3f4;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.detail-label {
|
||||
font-size: 0.85rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.detail-value {
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
font-family: 'SF Mono', 'Monaco', 'Inconsolata', monospace;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.card-flipper {
|
||||
position: relative;
|
||||
perspective: 1000px;
|
||||
min-height: 450px;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.front,
|
||||
.back {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
backface-visibility: hidden;
|
||||
transition: transform 0.8s ease-in-out;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.front {
|
||||
transform: rotateY(0deg);
|
||||
}
|
||||
|
||||
.back {
|
||||
transform: rotateY(180deg);
|
||||
}
|
||||
|
||||
&.flipped {
|
||||
.front {
|
||||
transform: rotateY(-180deg);
|
||||
}
|
||||
|
||||
.back {
|
||||
transform: rotateY(0deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.card-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.flip-btn {
|
||||
margin-top: auto;
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref } from 'vue'
|
||||
import { useSystemStore } from 'src/entities/System/model'
|
||||
import { ColorCard } from 'src/shared/ui'
|
||||
|
||||
const showDetails = ref(false)
|
||||
|
||||
@@ -253,11 +71,13 @@ const availablePercent = computed(() => {
|
||||
return (available.value / ramQuota.value) * 100
|
||||
})
|
||||
|
||||
const progressClass = computed(() => {
|
||||
// Цвет кольца: канон-статусы. Базовый — positive; при перегрузке квоты
|
||||
// переключаемся на предупреждающий и негативный.
|
||||
const gaugeColor = computed(() => {
|
||||
const percent = usagePercent.value
|
||||
if (percent >= 95) return 'critical-usage'
|
||||
if (percent >= 80) return 'high-usage'
|
||||
return ''
|
||||
if (percent >= 95) return 'negative'
|
||||
if (percent >= 80) return 'warning'
|
||||
return 'positive'
|
||||
})
|
||||
|
||||
// Примерная оценка стоимости дополнительного RAM (упрощенная логика)
|
||||
@@ -288,3 +108,12 @@ const formatCost = (cost: number): string => {
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@use './../../../shared/resource-card.scss';
|
||||
|
||||
.resource-card__icon {
|
||||
background: var(--p-pos-soft);
|
||||
color: var(--p-pos);
|
||||
}
|
||||
</style>
|
||||
|
||||
+120
-180
@@ -1,26 +1,23 @@
|
||||
<template lang="pug">
|
||||
.resource-info-widget
|
||||
.resource-info-card
|
||||
ColorCard(color='orange')
|
||||
| Внимание! Не допускайте исчерпание квот ресурсов — это заблокирует возможность обработки новых документов в кооперативе.
|
||||
.resource-info-card
|
||||
.resource-info-card__head
|
||||
.resource-info-card__icon
|
||||
q-icon(name="help_outline", size="20px")
|
||||
.resource-info-card__title Как это работает
|
||||
|
||||
.resource-info-description
|
||||
.resource-info-card__body
|
||||
p.resource-info-card__lead
|
||||
| Вычислительные ресурсы арендуются на 24 часа у делегатов блокчейн-платформы. Минимальная квота — 5 AXON в сутки.
|
||||
p.resource-info-card__text
|
||||
| Когда использование любого ресурса превышает 70% от доступной квоты, система автоматически арендует
|
||||
| дополнительную квоту за 5 AXON из вашего баланса.
|
||||
div.full-width.text-right
|
||||
q-btn(
|
||||
dense
|
||||
flat
|
||||
color="white"
|
||||
label="Как это работает"
|
||||
icon="help_outline"
|
||||
@click="showHowItWorksDialog = true"
|
||||
).q-ml-lg
|
||||
|
||||
BaseButton.resource-info-card__action(variant="secondary", size="sm", @click="showHowItWorksDialog = true")
|
||||
template(#icon-left)
|
||||
q-icon(name="menu_book", size="16px")
|
||||
| Подробнее о системе ресурсов
|
||||
|
||||
|
||||
// Диалог "Как это работает"
|
||||
//- Диалог "Как это работает"
|
||||
q-dialog(v-model="showHowItWorksDialog")
|
||||
ModalBase(title="Как работает система ресурсов", :style="dialogStyle")
|
||||
.q-pa-md
|
||||
@@ -234,18 +231,13 @@
|
||||
| Планируйте расходы заранее и отслеживайте фактическое потребление, чтобы оптимизировать затраты.
|
||||
|
||||
template(#actions)
|
||||
q-btn(
|
||||
flat
|
||||
label="Закрыть"
|
||||
color="primary"
|
||||
v-close-popup
|
||||
)
|
||||
|
||||
BaseButton(variant="ghost", v-close-popup) Закрыть
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { ModalBase, ColorCard } from 'src/shared/ui'
|
||||
import { ModalBase } from 'src/shared/ui'
|
||||
import { BaseButton } from 'src/shared/ui/base/BaseButton'
|
||||
|
||||
// Диалог "Как это работает"
|
||||
const showHowItWorksDialog = ref(false)
|
||||
@@ -259,80 +251,80 @@ const dialogStyle = ref({
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.resource-info-widget {
|
||||
.resource-info-card {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
border-radius: 16px;
|
||||
padding: 2rem;
|
||||
color: white;
|
||||
box-shadow: 0 8px 32px rgba(102, 126, 234, 0.3);
|
||||
|
||||
.resource-info-header {
|
||||
margin-bottom: 1.5rem;
|
||||
|
||||
.resource-info-title {
|
||||
color: white;
|
||||
margin-bottom: 0;
|
||||
font-size: 2.5rem;
|
||||
font-weight: 700;
|
||||
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
}
|
||||
|
||||
.resource-info-description {
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
font-size: 1.1rem;
|
||||
line-height: 1.6;
|
||||
margin-bottom: 1.5rem;
|
||||
|
||||
}
|
||||
|
||||
:deep(.color-card) {
|
||||
font-size: 1.1rem;
|
||||
line-height: 1.5;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
|
||||
.resource-actions {
|
||||
.how-it-works-btn {
|
||||
margin-top: 0.5rem;
|
||||
|
||||
&:hover {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
padding: 1.5rem;
|
||||
|
||||
.resource-info-header {
|
||||
.resource-info-title {
|
||||
font-size: 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
.resource-info-description {
|
||||
font-size: 1rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
/* Канон-карточка «Как это работает»: плоская surface, без градиента. */
|
||||
.resource-info-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--p-4, 16px);
|
||||
background: var(--p-surface);
|
||||
border: 1px solid var(--p-line);
|
||||
border-radius: var(--p-r-lg, 16px);
|
||||
padding: var(--p-5, 20px);
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.how-it-works-content {
|
||||
.intro-section,
|
||||
.resources-section,
|
||||
.automation-section,
|
||||
.conclusion-section {
|
||||
.text-h6 {
|
||||
font-weight: 600;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.resource-info-card__head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--p-3, 12px);
|
||||
}
|
||||
|
||||
p {
|
||||
line-height: 1.6;
|
||||
}
|
||||
.resource-info-card__icon {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
flex: 0 0 40px;
|
||||
border-radius: var(--p-r-md, 12px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: var(--p-primary-soft);
|
||||
color: var(--p-primary);
|
||||
}
|
||||
|
||||
.resource-info-card__title {
|
||||
font-size: var(--p-fs-h2, 18px);
|
||||
font-weight: 600;
|
||||
color: var(--p-ink);
|
||||
}
|
||||
|
||||
.resource-info-card__body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--p-2, 8px);
|
||||
}
|
||||
|
||||
.resource-info-card__lead {
|
||||
margin: 0;
|
||||
font-size: var(--p-fs-body, 14px);
|
||||
font-weight: 500;
|
||||
color: var(--p-ink);
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
.resource-info-card__text {
|
||||
margin: 0;
|
||||
font-size: var(--p-fs-body-sm, 13px);
|
||||
color: var(--p-ink-2);
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
.resource-info-card__action {
|
||||
align-self: flex-start;
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
/* — Содержимое диалога — канон-токены вместо хардкод-цветов — */
|
||||
.how-it-works-content {
|
||||
color: var(--p-ink-1);
|
||||
|
||||
.text-h6 {
|
||||
font-weight: 600;
|
||||
margin-bottom: var(--p-4, 16px);
|
||||
color: var(--p-ink);
|
||||
}
|
||||
|
||||
p {
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.resource-explanation,
|
||||
@@ -340,90 +332,64 @@ const dialogStyle = ref({
|
||||
.explanation-item {
|
||||
.text-subtitle1 {
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.5rem;
|
||||
margin-bottom: var(--p-2, 8px);
|
||||
color: var(--p-ink);
|
||||
}
|
||||
|
||||
p {
|
||||
margin-left: 1.5rem;
|
||||
padding-left: 0.5rem;
|
||||
border-left: 3px solid #667eea;
|
||||
background: rgba(102, 126, 234, 0.05);
|
||||
padding: 0.5rem 0 0.5rem 1rem;
|
||||
margin-left: var(--p-6, 24px);
|
||||
border-left: 3px solid var(--p-primary-line);
|
||||
background: var(--p-primary-soft);
|
||||
border-radius: 0 var(--p-r-sm, 8px) var(--p-r-sm, 8px) 0;
|
||||
padding: var(--p-2, 8px) 0 var(--p-2, 8px) var(--p-4, 16px);
|
||||
line-height: 1.6;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.important-note {
|
||||
background: #fff3cd;
|
||||
border: 1px solid #ffeaa7;
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
background: var(--p-warn-soft);
|
||||
border: 1px solid var(--p-warn);
|
||||
border-radius: var(--p-r-md, 12px);
|
||||
padding: var(--p-4, 16px);
|
||||
|
||||
ul {
|
||||
margin-top: 0.5rem;
|
||||
padding-left: 1.5rem;
|
||||
margin-top: var(--p-2, 8px);
|
||||
padding-left: var(--p-6, 24px);
|
||||
|
||||
li {
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.benefits-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 1rem;
|
||||
|
||||
.benefit-item {
|
||||
background: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
text-align: center;
|
||||
|
||||
.text-subtitle2 {
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
font-size: 0.9rem;
|
||||
margin-bottom: var(--p-1, 4px);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.faq-section {
|
||||
.text-h6 {
|
||||
font-weight: 600;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
:deep(.q-expansion-item) {
|
||||
margin-bottom: 0.75rem;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 8px;
|
||||
margin-bottom: var(--p-3, 12px);
|
||||
border: 1px solid var(--p-line-1);
|
||||
border-radius: var(--p-r-md, 12px);
|
||||
overflow: hidden;
|
||||
|
||||
.q-expansion-item__container {
|
||||
.q-item {
|
||||
background: #f8f9fa;
|
||||
font-weight: 500;
|
||||
.q-item {
|
||||
background: var(--p-surface-2);
|
||||
font-weight: 500;
|
||||
color: var(--p-ink);
|
||||
|
||||
&:hover {
|
||||
background: #e9ecef;
|
||||
}
|
||||
&:hover {
|
||||
background: var(--p-surface-3);
|
||||
}
|
||||
}
|
||||
|
||||
.q-card {
|
||||
background: white;
|
||||
background: var(--p-surface);
|
||||
box-shadow: none;
|
||||
|
||||
.q-card__section {
|
||||
line-height: 1.6;
|
||||
color: var(--p-ink-1);
|
||||
|
||||
p {
|
||||
margin-bottom: 0.75rem;
|
||||
margin-bottom: var(--p-3, 12px);
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
@@ -431,54 +397,28 @@ const dialogStyle = ref({
|
||||
}
|
||||
|
||||
ul, ol {
|
||||
margin-top: 0.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
padding-left: 1.5rem;
|
||||
margin-top: var(--p-2, 8px);
|
||||
margin-bottom: var(--p-2, 8px);
|
||||
padding-left: var(--p-6, 24px);
|
||||
|
||||
li {
|
||||
margin-bottom: 0.5rem;
|
||||
margin-bottom: var(--p-2, 8px);
|
||||
line-height: 1.5;
|
||||
}
|
||||
}
|
||||
|
||||
b {
|
||||
color: #667eea;
|
||||
color: var(--p-primary);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.text-negative {
|
||||
color: #c10015;
|
||||
color: var(--p-neg);
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.delegates-section,
|
||||
.accounts-section {
|
||||
.text-h6 {
|
||||
font-weight: 600;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.resource-explanation {
|
||||
.explanation-item {
|
||||
.text-subtitle1 {
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-left: 1.5rem;
|
||||
padding-left: 0.5rem;
|
||||
border-left: 3px solid #667eea;
|
||||
background: rgba(102, 126, 234, 0.05);
|
||||
padding: 0.5rem 0 0.5rem 1rem;
|
||||
line-height: 1.6;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template lang="pug">
|
||||
div.page-shell
|
||||
q-card.q-mt-md(flat)
|
||||
.accounts-page
|
||||
q-card(flat)
|
||||
q-table.full-height(
|
||||
flat
|
||||
:grid='isMobile'
|
||||
@@ -43,7 +43,7 @@ div.page-shell
|
||||
.q-pa-sm
|
||||
.row.items-center.q-mb-sm
|
||||
.col
|
||||
.text-caption.text-grey-6 История проводок
|
||||
.text-caption.t-faint История проводок
|
||||
.col-auto
|
||||
q-btn(
|
||||
flat dense size='sm' color='primary'
|
||||
@@ -81,7 +81,7 @@ div.page-shell
|
||||
@click='copyFullHash(cp.row.processHash)'
|
||||
)
|
||||
q-tooltip Клик — копировать полный хэш
|
||||
span.text-grey-6(v-else) —
|
||||
span.t-faint(v-else) —
|
||||
template(#body-cell-quantity='cp')
|
||||
q-td.text-right(:props='cp') {{ cp.row.quantity ? formatAsset2Digits(cp.row.quantity) : '—' }}
|
||||
template(#body-cell-createdAt='cp')
|
||||
@@ -115,13 +115,13 @@ div.page-shell
|
||||
q-tooltip К операциям счёта
|
||||
.row.q-mt-sm
|
||||
.col-4
|
||||
.text-caption.text-grey-6 Дебет
|
||||
.text-caption.t-faint Дебет
|
||||
.text-body2 {{ formatAsset2Digits(props.row.debitBalance) }}
|
||||
.col-4
|
||||
.text-caption.text-grey-6 Кредит
|
||||
.text-caption.t-faint Кредит
|
||||
.text-body2 {{ formatAsset2Digits(props.row.creditBalance) }}
|
||||
.col-4
|
||||
.text-caption.text-grey-6 Сальдо
|
||||
.text-caption.t-faint Сальдо
|
||||
.text-body2.text-weight-bold {{ formatAsset2Digits(props.row.balance) }}
|
||||
</template>
|
||||
|
||||
@@ -234,19 +234,23 @@ onMounted(async () => {
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.accounts-page {
|
||||
padding: var(--p-6, 24px);
|
||||
@media (max-width: 768px) {
|
||||
padding: var(--p-4, 16px);
|
||||
}
|
||||
}
|
||||
.font-monospace {
|
||||
font-family: 'JetBrains Mono', 'Courier New', monospace;
|
||||
letter-spacing: 0.03em;
|
||||
}
|
||||
// Тип счёта (Активный/Пассивный) и сторона проводки (Дебет/Кредит): subtle
|
||||
// hue-различие при максимальной читаемости. quasar text-blue-grey-8 /
|
||||
// text-brown-7 нечитаемы на тёмной теме — здесь theme-aware-варианты.
|
||||
// Тип счёта (Активный/Пассивный) и сторона проводки (Дебет/Кредит):
|
||||
// различие словом + спокойный канон-контраст. Активный/Дебет — основной
|
||||
// ink, Пассивный/Кредит — приглушённый; оба theme-aware через токены.
|
||||
.account-type-active {
|
||||
color: #37474F; // blue-grey-9
|
||||
.body--dark & { color: #B0BEC5; } // blue-grey-3
|
||||
color: var(--p-ink);
|
||||
}
|
||||
.account-type-passive {
|
||||
color: #5D4037; // brown-8
|
||||
.body--dark & { color: #BCAAA4; } // brown-3
|
||||
color: var(--p-ink-2);
|
||||
}
|
||||
</style>
|
||||
|
||||
+26
-29
@@ -1,5 +1,5 @@
|
||||
<template lang="pug">
|
||||
.q-pa-md
|
||||
.documents-archive
|
||||
.row.items-center.q-mb-md
|
||||
.text-h6.col Архив сгенерированных отчётов
|
||||
|
||||
@@ -28,36 +28,32 @@
|
||||
@update:model-value='onFilterChange'
|
||||
)
|
||||
|
||||
q-table(
|
||||
:rows='reportStore.archive.items'
|
||||
:columns='archiveColumns'
|
||||
row-key='id'
|
||||
flat
|
||||
:loading='reportStore.archiveLoading'
|
||||
:pagination='archivePagination'
|
||||
@request='onArchiveRequest'
|
||||
)
|
||||
template(#body-cell-valid='props')
|
||||
q-td(:props='props')
|
||||
q-chip(
|
||||
:color='props.row.isValid ? "positive" : "negative"'
|
||||
text-color='white'
|
||||
dense
|
||||
size='sm'
|
||||
) {{ props.row.isValid ? 'Валиден' : 'Ошибки' }}
|
||||
q-card(flat)
|
||||
q-table(
|
||||
:rows='reportStore.archive.items'
|
||||
:columns='archiveColumns'
|
||||
row-key='id'
|
||||
flat
|
||||
:loading='reportStore.archiveLoading'
|
||||
:pagination='archivePagination'
|
||||
@request='onArchiveRequest'
|
||||
)
|
||||
template(#body-cell-valid='props')
|
||||
q-td(:props='props')
|
||||
BaseBadge(:variant='props.row.isValid ? "pos" : "neg"') {{ props.row.isValid ? 'Валиден' : 'Ошибки' }}
|
||||
|
||||
template(#body-cell-createdAt='props')
|
||||
q-td(:props='props') {{ formatDate(props.row.createdAt) }}
|
||||
template(#body-cell-createdAt='props')
|
||||
q-td(:props='props') {{ formatDate(props.row.createdAt) }}
|
||||
|
||||
template(#body-cell-actions='props')
|
||||
q-td(:props='props')
|
||||
q-btn(
|
||||
flat dense
|
||||
icon='fa-solid fa-download'
|
||||
color='primary'
|
||||
@click='downloadArchive(props.row.id)'
|
||||
)
|
||||
q-tooltip Скачать XML
|
||||
template(#body-cell-actions='props')
|
||||
q-td(:props='props')
|
||||
q-btn(
|
||||
flat dense
|
||||
icon='fa-solid fa-download'
|
||||
color='primary'
|
||||
@click='downloadArchive(props.row.id)'
|
||||
)
|
||||
q-tooltip Скачать XML
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
@@ -68,6 +64,7 @@ import {
|
||||
useReportStore,
|
||||
type IReportType,
|
||||
} from 'src/entities/Report'
|
||||
import { BaseBadge } from 'src/shared/ui/base/BaseBadge'
|
||||
|
||||
const MVP_REPORT_TYPES = ['BUHOTCH', 'NDFL6', 'RSV', 'PSV', 'FSS4'] as IReportType[]
|
||||
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
<template lang="pug">
|
||||
.q-pa-md
|
||||
.documents-calendar
|
||||
ReportsCalendar(ref='calendarRef' @select='onCalendarSelect')
|
||||
|
||||
ReportEditorDialog(
|
||||
|
||||
+52
-49
@@ -1,58 +1,59 @@
|
||||
<template lang="pug">
|
||||
.q-pa-md
|
||||
.documents-forms
|
||||
.row.items-center.q-mb-md
|
||||
.text-h6.col Доступные формы
|
||||
q-btn(flat dense icon='fa-solid fa-rotate' @click='loadReports' :loading='reportStore.loading')
|
||||
q-tooltip Обновить
|
||||
|
||||
q-table(
|
||||
:rows='visibleReports'
|
||||
:columns='columns'
|
||||
row-key='type'
|
||||
flat
|
||||
:loading='reportStore.loading'
|
||||
hide-pagination
|
||||
:pagination='{ rowsPerPage: 0 }'
|
||||
)
|
||||
template(#body-cell-period='props')
|
||||
q-td(:props='props')
|
||||
q-chip(:color='periodColor(props.row.period)' text-color='white' dense) {{ periodLabel(props.row.period) }}
|
||||
q-card(flat)
|
||||
q-table(
|
||||
:rows='visibleReports'
|
||||
:columns='columns'
|
||||
row-key='type'
|
||||
flat
|
||||
:loading='reportStore.loading'
|
||||
hide-pagination
|
||||
:pagination='{ rowsPerPage: 0 }'
|
||||
)
|
||||
template(#body-cell-period='props')
|
||||
q-td(:props='props')
|
||||
BaseBadge(:variant='periodVariant(props.row.period)') {{ periodLabel(props.row.period) }}
|
||||
|
||||
template(#body-cell-ready='props')
|
||||
q-td(:props='props')
|
||||
q-icon(
|
||||
v-if='props.row.readyToGenerate'
|
||||
name='fa-solid fa-check-circle'
|
||||
color='positive'
|
||||
size='20px'
|
||||
)
|
||||
q-tooltip Реквизиты заполнены
|
||||
q-icon(
|
||||
v-else
|
||||
name='fa-solid fa-triangle-exclamation'
|
||||
color='warning'
|
||||
size='20px'
|
||||
)
|
||||
q-tooltip {{ missingTooltip(props.row) }}
|
||||
template(#body-cell-ready='props')
|
||||
q-td(:props='props')
|
||||
q-icon(
|
||||
v-if='props.row.readyToGenerate'
|
||||
name='fa-solid fa-check-circle'
|
||||
color='positive'
|
||||
size='20px'
|
||||
)
|
||||
q-tooltip Реквизиты заполнены
|
||||
q-icon(
|
||||
v-else
|
||||
name='fa-solid fa-triangle-exclamation'
|
||||
color='warning'
|
||||
size='20px'
|
||||
)
|
||||
q-tooltip {{ missingTooltip(props.row) }}
|
||||
|
||||
template(#body-cell-actions='props')
|
||||
q-td(:props='props')
|
||||
q-btn(
|
||||
v-if='props.row.readyToGenerate'
|
||||
flat dense
|
||||
icon='fa-solid fa-pen-to-square'
|
||||
color='primary'
|
||||
@click='openEditor(props.row)'
|
||||
)
|
||||
q-tooltip Открыть редактор формы
|
||||
q-btn(
|
||||
v-else
|
||||
flat dense
|
||||
icon='fa-solid fa-gear'
|
||||
color='warning'
|
||||
:to='{ name: "reports-settings", query: { focus: firstMissing(props.row) } }'
|
||||
)
|
||||
q-tooltip Заполнить реквизиты
|
||||
template(#body-cell-actions='props')
|
||||
q-td(:props='props')
|
||||
q-btn(
|
||||
v-if='props.row.readyToGenerate'
|
||||
flat dense
|
||||
icon='fa-solid fa-pen-to-square'
|
||||
color='primary'
|
||||
@click='openEditor(props.row)'
|
||||
)
|
||||
q-tooltip Открыть редактор формы
|
||||
q-btn(
|
||||
v-else
|
||||
flat dense
|
||||
icon='fa-solid fa-gear'
|
||||
color='warning'
|
||||
:to='{ name: "reports-settings", query: { focus: firstMissing(props.row) } }'
|
||||
)
|
||||
q-tooltip Заполнить реквизиты
|
||||
|
||||
ReportEditorDialog(
|
||||
v-if='showEditor'
|
||||
@@ -74,6 +75,8 @@ import {
|
||||
type IAvailableReport,
|
||||
type IReportType,
|
||||
} from 'src/entities/Report'
|
||||
import { BaseBadge } from 'src/shared/ui/base/BaseBadge'
|
||||
import type { BaseBadgeProps } from 'src/shared/ui/base/BaseBadge/BaseBadge.types'
|
||||
import ReportEditorDialog from './ReportEditorDialog.vue'
|
||||
|
||||
const MVP_REPORT_TYPES = ['BUHOTCH', 'NDFL6', 'RSV', 'PSV', 'FSS4'] as IReportType[]
|
||||
@@ -102,8 +105,8 @@ function periodLabel(p: string) {
|
||||
return ({ yearly: 'Ежегодно', quarterly: 'Ежеквартально', monthly: 'Ежемесячно' }[p] ?? p)
|
||||
}
|
||||
|
||||
function periodColor(p: string) {
|
||||
return ({ yearly: 'deep-purple', quarterly: 'blue', monthly: 'teal' }[p] ?? 'grey')
|
||||
function periodVariant(p: string): BaseBadgeProps['variant'] {
|
||||
return ({ yearly: 'accent', quarterly: 'info', monthly: 'pos' }[p] ?? 'neutral') as BaseBadgeProps['variant']
|
||||
}
|
||||
|
||||
function missingTooltip(r: IAvailableReport) {
|
||||
|
||||
@@ -1,69 +1,37 @@
|
||||
<template lang="pug">
|
||||
.column.flex-1.min-h-0.min-w-0.no-wrap
|
||||
router-view
|
||||
.reports-shell
|
||||
SecondLevelTabs(:tabs='tabs')
|
||||
.reports-shell__content
|
||||
router-view
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, markRaw, onBeforeUnmount, onMounted, watch } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { useHeaderActions } from 'src/shared/hooks/useHeaderActions'
|
||||
import RouteMenuButton from 'src/shared/ui/RouteMenuButton/RouteMenuButton.vue'
|
||||
import { SecondLevelTabs } from 'src/shared/ui/SecondLevelTabs'
|
||||
|
||||
// DocumentsPage — shell-страница для 3 подвкладок отчётности. Инжектирует
|
||||
// 3 кнопки в шапку сайта, переключение — через именованные маршруты
|
||||
// reports-documents-calendar / -forms / -archive. Рендер содержимого — через
|
||||
// <router-view>. Паттерн один-в-один как capital/ProjectPage.
|
||||
|
||||
const route = useRoute()
|
||||
const { registerAction, clearActions } = useHeaderActions()
|
||||
|
||||
const menuButtons = computed(() => [
|
||||
{
|
||||
id: 'reports-documents-calendar-menu',
|
||||
component: markRaw(RouteMenuButton),
|
||||
props: {
|
||||
routeName: 'reports-documents-calendar',
|
||||
label: 'Календарь',
|
||||
},
|
||||
order: 1,
|
||||
},
|
||||
{
|
||||
id: 'reports-documents-forms-menu',
|
||||
component: markRaw(RouteMenuButton),
|
||||
props: {
|
||||
routeName: 'reports-documents-forms',
|
||||
label: 'Список форм',
|
||||
},
|
||||
order: 2,
|
||||
},
|
||||
{
|
||||
id: 'reports-documents-archive-menu',
|
||||
component: markRaw(RouteMenuButton),
|
||||
props: {
|
||||
routeName: 'reports-documents-archive',
|
||||
label: 'Архив',
|
||||
},
|
||||
order: 3,
|
||||
},
|
||||
])
|
||||
|
||||
function registerAll() {
|
||||
menuButtons.value.forEach((b) => registerAction(b))
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
registerAll()
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
clearActions()
|
||||
})
|
||||
|
||||
// При переходе между своими child-маршрутами подсветка обновляется
|
||||
// самим RouteMenuButton через route.name; дополнительной регистрации не надо.
|
||||
// Но если кто-то попадёт на другую страницу (напр. reports-settings),
|
||||
// clearActions() в onBeforeUnmount shell'а сам всё вычистит.
|
||||
watch(() => route.name, () => {
|
||||
// no-op: оставлено как точка расширения, если понадобится условная регистрация.
|
||||
})
|
||||
// Shell-страница «Отчётность»: канон-меню второго уровня
|
||||
// (Календарь / Список форм / Архив). Содержимое — через <router-view>.
|
||||
const tabs = [
|
||||
{ routeName: 'reports-documents-calendar', label: 'Календарь', icon: 'fa-solid fa-calendar-days' },
|
||||
{ routeName: 'reports-documents-forms', label: 'Список форм', icon: 'fa-solid fa-list' },
|
||||
{ routeName: 'reports-documents-archive', label: 'Архив', icon: 'fa-solid fa-box-archive' },
|
||||
]
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.reports-shell {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
min-width: 0;
|
||||
}
|
||||
.reports-shell__content {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
padding: var(--p-6, 24px);
|
||||
@media (max-width: 768px) {
|
||||
padding: var(--p-4, 16px);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
+18
-18
@@ -47,7 +47,7 @@ q-dialog(
|
||||
.stub-requisites(v-if='!readinessLoading && notReady')
|
||||
q-icon(name='fa-solid fa-circle-info' size='56px' color='orange')
|
||||
.text-h6.q-mt-md.q-mb-sm Сначала заполните реквизиты
|
||||
.text-body2.text-grey-8.q-mb-md
|
||||
.text-body2.t-muted.q-mb-md
|
||||
| Для отчёта «{{ reportTitle }}» не хватает обязательных полей. Заполните их в разделе «Реквизиты», после чего вернитесь сюда.
|
||||
.missing-chips.q-mb-lg
|
||||
q-chip(
|
||||
@@ -735,15 +735,15 @@ function goToRequisites(): void {
|
||||
<style scoped lang="scss">
|
||||
.editor-container {
|
||||
overflow: auto;
|
||||
background: #f5f6f8;
|
||||
background: var(--p-canvas);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.action-panel {
|
||||
width: 260px;
|
||||
flex: 0 0 260px;
|
||||
border-left: 1px solid rgba(0, 0, 0, 0.08);
|
||||
background: var(--q-neutral-2, #fafafa);
|
||||
border-left: 1px solid var(--p-line);
|
||||
background: var(--p-surface-2);
|
||||
}
|
||||
|
||||
// Mobile: панель становится slide-in overlay справа. Без action-panel--open
|
||||
@@ -756,7 +756,7 @@ function goToRequisites(): void {
|
||||
width: min(85vw, 320px);
|
||||
flex: none;
|
||||
z-index: 6001; // выше q-dialog (которое ~6000); иначе поверх не ложится
|
||||
box-shadow: -4px 0 16px rgba(0, 0, 0, 0.18);
|
||||
box-shadow: var(--p-shadow-modal, -4px 0 16px rgba(0, 0, 0, 0.18));
|
||||
transform: translateX(100%);
|
||||
transition: transform 0.22s ease-out;
|
||||
overflow-y: auto;
|
||||
@@ -769,7 +769,7 @@ function goToRequisites(): void {
|
||||
.action-backdrop {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
background: var(--p-overlay, rgba(0, 0, 0, 0.4));
|
||||
z-index: 6000;
|
||||
animation: fade-in 0.2s ease-out;
|
||||
}
|
||||
@@ -790,7 +790,7 @@ function goToRequisites(): void {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
color: #666;
|
||||
color: var(--p-ink-2);
|
||||
text-align: center;
|
||||
padding: 40px;
|
||||
}
|
||||
@@ -804,7 +804,7 @@ function goToRequisites(): void {
|
||||
margin: 48px auto;
|
||||
padding: 32px;
|
||||
text-align: center;
|
||||
color: #333;
|
||||
color: var(--p-ink-1);
|
||||
|
||||
.missing-chips {
|
||||
display: flex;
|
||||
@@ -826,27 +826,27 @@ function goToRequisites(): void {
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 12px;
|
||||
border-radius: 4px;
|
||||
font-size: 13px;
|
||||
border-radius: var(--p-r-xs, 4px);
|
||||
font-size: var(--p-fs-body-sm, 13px);
|
||||
font-weight: 500;
|
||||
&.ok {
|
||||
background: #e8f5e9;
|
||||
color: #2e7d32;
|
||||
background: var(--p-pos-soft);
|
||||
color: var(--p-pos);
|
||||
}
|
||||
&.bad {
|
||||
background: #ffebee;
|
||||
color: #c62828;
|
||||
background: var(--p-neg-soft);
|
||||
color: var(--p-neg);
|
||||
}
|
||||
}
|
||||
|
||||
.mark-hint {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
font-size: var(--p-fs-meta, 12px);
|
||||
color: var(--p-ink-2);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 6px 8px;
|
||||
background: #eceff1;
|
||||
border-radius: 4px;
|
||||
background: var(--p-surface-2);
|
||||
border-radius: var(--p-r-xs, 4px);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template lang="pug">
|
||||
div.page-shell
|
||||
.operations-page
|
||||
//- Активные фильтры (чипы) + фильтры по датам
|
||||
q-card.q-mt-md(flat)
|
||||
q-card(flat)
|
||||
q-card-section
|
||||
.row.q-gutter-sm.items-center.q-mb-sm(
|
||||
v-if='filters.accountId !== null || filters.walletName || filters.processHash || filters.applyGlobalSequence || filters.username'
|
||||
@@ -146,7 +146,7 @@ div.page-shell
|
||||
@click='copyFullHash(props.row.processHash)'
|
||||
)
|
||||
q-tooltip Клик — копировать полный хэш
|
||||
span.text-grey-6(v-else) —
|
||||
span.t-faint(v-else) —
|
||||
q-td {{ formatDate(props.row.createdAt) }}
|
||||
q-td
|
||||
q-chip(
|
||||
@@ -172,14 +172,14 @@ div.page-shell
|
||||
)
|
||||
.text-h6.text-weight-medium {{ rowLabel(props.row) }}
|
||||
.row.items-center.q-gutter-sm.q-mb-xs(v-if='props.row.operationCode')
|
||||
.text-caption.text-grey-7 Тип процесса:
|
||||
.text-caption.t-muted Тип процесса:
|
||||
EntityIdBadge(
|
||||
:rawId='props.row.operationCode'
|
||||
copy-on-click
|
||||
)
|
||||
q-tooltip Клик — копировать
|
||||
.row.items-center.q-gutter-sm.q-mb-xs(v-if='props.row.processHash')
|
||||
.text-caption.text-grey-7 ID процесса:
|
||||
.text-caption.t-muted ID процесса:
|
||||
EntityIdBadge(
|
||||
:rawId='props.row.processHash'
|
||||
copy-on-click
|
||||
@@ -187,7 +187,7 @@ div.page-shell
|
||||
q-tooltip Клик — копировать
|
||||
.row.items-center.q-gutter-sm(v-if='props.row.memo')
|
||||
q-icon(name='fa-solid fa-note-sticky' color='grey-6' size='xs')
|
||||
.text-caption.text-grey-8 {{ props.row.memo }}
|
||||
.text-caption.t-muted {{ props.row.memo }}
|
||||
|
||||
//- Тело: две таблицы рядом на md+, друг под другом на маленьких
|
||||
template(v-if='childLoading.get(props.row.globalSequence)')
|
||||
@@ -219,7 +219,7 @@ div.page-shell
|
||||
WalletIdCell(:wallet-name='cp.row.walletTo')
|
||||
template(#body-cell-quantity='cp')
|
||||
q-td.text-right(:props='cp') {{ formatAmount(cp.row.quantity) }}
|
||||
.text-caption.text-grey-6(v-else) Движений по кошелькам нет
|
||||
.text-caption.t-faint(v-else) Движений по кошелькам нет
|
||||
|
||||
//- Таблица 2: Проводки по счетам (Дт → Кт парами)
|
||||
.col-12.col-md-6
|
||||
@@ -244,28 +244,28 @@ div.page-shell
|
||||
AccountIdCell(:account-code='cp.row.creditCode')
|
||||
template(#body-cell-quantity='cp')
|
||||
q-td.text-right(:props='cp') {{ formatAmount(cp.row.quantity) }}
|
||||
.text-caption.text-grey-6(v-else) Проводок нет
|
||||
.text-caption.t-faint(v-else) Проводок нет
|
||||
|
||||
template(#item='props')
|
||||
.col-12
|
||||
q-card.q-pa-md.q-mb-sm
|
||||
.row.items-center.q-gutter-x-md
|
||||
.col
|
||||
.text-caption.text-grey-6 {{ formatDate(props.row.createdAt) }}
|
||||
.text-caption.t-faint {{ formatDate(props.row.createdAt) }}
|
||||
.text-body2.text-weight-medium {{ actionLabel(props.row.operationCode) }}
|
||||
.text-caption.text-grey-6.font-monospace {{ props.row.operationCode || '-' }}
|
||||
.text-caption.t-faint.font-monospace {{ props.row.operationCode || '-' }}
|
||||
.col-auto.text-right
|
||||
.text-caption.text-grey-6 Сумма
|
||||
.text-caption.t-faint Сумма
|
||||
.text-body1.text-weight-bold.font-monospace {{ formatAmount(props.row.quantity) }}
|
||||
.col-12.text-caption.text-grey-7
|
||||
.col-12.text-caption.t-muted
|
||||
| Пайщик: {{ fioCache.get(props.row.username ?? '') || props.row.username || '-' }}
|
||||
.col-12.row.q-gutter-xs.q-mt-xs.items-center
|
||||
.text-caption.text-grey-7 № операции
|
||||
.text-caption.t-muted № операции
|
||||
EntityIdBadge(
|
||||
:rawId='props.row.globalSequence'
|
||||
@click='copyText(String(props.row.globalSequence))'
|
||||
)
|
||||
.text-caption.text-grey-7 № процесса
|
||||
.text-caption.t-muted № процесса
|
||||
EntityIdBadge(
|
||||
v-if='props.row.processHash'
|
||||
:rawId='shortHash(props.row.processHash)'
|
||||
@@ -831,12 +831,18 @@ onMounted(async () => {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.operations-page {
|
||||
padding: var(--p-6, 24px);
|
||||
}
|
||||
@media (max-width: 768px) {
|
||||
.operations-page { padding: var(--p-4, 16px); }
|
||||
}
|
||||
.font-monospace {
|
||||
font-family: 'JetBrains Mono', 'Courier New', monospace;
|
||||
letter-spacing: 0.03em;
|
||||
}
|
||||
.op-header {
|
||||
border-left: 4px solid #9e9e9e;
|
||||
border-left: 4px solid var(--p-line-2);
|
||||
padding: 4px 0 4px 12px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
<template lang="pug">
|
||||
div.page-shell
|
||||
.postings-page
|
||||
//- Поиск по «номеру»: умный input — определяет тип ID по формату.
|
||||
//- Числовой bigint (≤24 цифр) → debit.global_sequence (точечно одна проводка).
|
||||
//- 64-символьная hex-строка → process_hash (все проводки одной операции).
|
||||
//- Любая другая строка с операционными префиксами (o.cap...) — игнорируем
|
||||
//- (для этого есть реестр операций). Фильтры account_id/username прилетают
|
||||
//- через cross-link.
|
||||
q-card.q-mt-md(flat)
|
||||
q-card(flat)
|
||||
q-card-section
|
||||
.row.q-gutter-sm.items-center.q-mb-sm(
|
||||
v-if='filters.accountId !== null || filters.username || filters.debitGlobalSequence || filters.applyGlobalSequence || filters.processHash'
|
||||
@@ -137,7 +137,7 @@ div.page-shell
|
||||
@click='copyText(String(props.row.debitGlobalSequence))'
|
||||
)
|
||||
q-tooltip Клик — копировать
|
||||
span.text-grey-6(v-else) —
|
||||
span.t-faint(v-else) —
|
||||
q-td
|
||||
EntityIdBadge(
|
||||
v-if='props.row.processHash'
|
||||
@@ -145,7 +145,7 @@ div.page-shell
|
||||
@click='copyFullHash(props.row.processHash)'
|
||||
)
|
||||
q-tooltip Клик — копировать полный хэш
|
||||
span.text-grey-6(v-else) —
|
||||
span.t-faint(v-else) —
|
||||
q-td
|
||||
q-chip(
|
||||
v-if='props.row.operationCode'
|
||||
@@ -154,7 +154,7 @@ div.page-shell
|
||||
:color='processChipBg(props.row.operationCode)'
|
||||
:text-color='processChipText(props.row.operationCode)'
|
||||
) {{ operationLabel(props.row.operationCode) }}
|
||||
span.text-grey-6(v-else) —
|
||||
span.t-faint(v-else) —
|
||||
q-td.text-center
|
||||
AccountIdCell(:account-code='debitCode(props.row.debitAccountId)')
|
||||
q-td.text-center
|
||||
@@ -176,10 +176,10 @@ div.page-shell
|
||||
q-card.q-pa-md.q-mb-sm
|
||||
.row.items-center.q-gutter-x-md
|
||||
.col
|
||||
.text-caption.text-grey-6 {{ formatDate(props.row.createdAt) }}
|
||||
.text-caption.t-faint {{ formatDate(props.row.createdAt) }}
|
||||
.text-body2.text-weight-medium {{ operationLabel(props.row.operationCode) }}
|
||||
.col-auto.text-right
|
||||
.text-caption.text-grey-6 Сумма
|
||||
.text-caption.t-faint Сумма
|
||||
.text-body1.text-weight-bold.font-monospace {{ formatAmount(props.row.quantity) }}
|
||||
.col-auto
|
||||
q-btn(
|
||||
@@ -190,22 +190,22 @@ div.page-shell
|
||||
:to='{ name: "reports-operations", query: { operation_id: props.row.parentApplyGlobalSequence } }'
|
||||
)
|
||||
.row.q-mt-sm.items-center.q-gutter-x-md
|
||||
.col-auto.text-caption.text-grey-7 Дебет
|
||||
.col-auto.text-caption.t-muted Дебет
|
||||
.col-auto
|
||||
AccountIdCell(:account-code='debitCode(props.row.debitAccountId)')
|
||||
.col-auto.text-caption.text-grey-7 Кредит
|
||||
.col-auto.text-caption.t-muted Кредит
|
||||
.col-auto
|
||||
AccountIdCell(:account-code='creditCode(props.row.creditAccountId)')
|
||||
.col-12.text-caption.text-grey-7
|
||||
.col-12.text-caption.t-muted
|
||||
| Пайщик: {{ fioCache.get(props.row.username ?? '') || props.row.username || '—' }}
|
||||
.col-12.row.q-gutter-xs.q-mt-xs.items-center
|
||||
.text-caption.text-grey-7 № проводки
|
||||
.text-caption.t-muted № проводки
|
||||
EntityIdBadge(
|
||||
v-if='props.row.debitGlobalSequence'
|
||||
:rawId='props.row.debitGlobalSequence'
|
||||
@click='copyText(String(props.row.debitGlobalSequence))'
|
||||
)
|
||||
.text-caption.text-grey-7 № процесса
|
||||
.text-caption.t-muted № процесса
|
||||
EntityIdBadge(
|
||||
v-if='props.row.processHash'
|
||||
:rawId='shortHash(props.row.processHash)'
|
||||
@@ -599,6 +599,12 @@ onMounted(async () => {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.postings-page {
|
||||
padding: var(--p-6, 24px);
|
||||
}
|
||||
@media (max-width: 768px) {
|
||||
.postings-page { padding: var(--p-4, 16px); }
|
||||
}
|
||||
.font-monospace {
|
||||
font-family: 'JetBrains Mono', 'Courier New', monospace;
|
||||
letter-spacing: 0.03em;
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<template lang="pug">
|
||||
q-form.page-shell(@submit.prevent='save' @validation-error='onValidationError' greedy)
|
||||
q-form.settings-page(@submit.prevent='save' @validation-error='onValidationError' greedy)
|
||||
//- Реквизиты организации (read-only — берутся из профиля кооператива)
|
||||
q-card.q-mt-md(flat)
|
||||
q-card-section.q-py-sm
|
||||
.text-h6 Реквизиты организации
|
||||
.text-caption.text-grey-7 Справочные данные кооператива — не редактируются
|
||||
.text-caption.t-muted Справочные данные кооператива — не редактируются
|
||||
|
||||
q-separator
|
||||
|
||||
@@ -23,7 +23,7 @@ q-form.page-shell(@submit.prevent='save' @validation-error='onValidationError' g
|
||||
q-card.q-mt-md(flat)
|
||||
q-card-section.q-py-sm
|
||||
.text-h6 Классификаторы
|
||||
.text-caption.text-grey-7 ОКВЭД, ОКФС, ОКОПФ, ОКТМО, ОКПО — обязательны для большинства отчётов ФНС
|
||||
.text-caption.t-muted ОКВЭД, ОКФС, ОКОПФ, ОКТМО, ОКПО — обязательны для большинства отчётов ФНС
|
||||
|
||||
q-separator
|
||||
|
||||
@@ -50,7 +50,7 @@ q-form.page-shell(@submit.prevent='save' @validation-error='onValidationError' g
|
||||
q-card.q-mt-md(flat)
|
||||
q-card-section.q-py-sm
|
||||
.text-h6 СФР (для ЕФС-1)
|
||||
.text-caption.text-grey-7 Регистрационный номер СФР и должность председателя
|
||||
.text-caption.t-muted Регистрационный номер СФР и должность председателя
|
||||
|
||||
q-separator
|
||||
|
||||
@@ -81,7 +81,7 @@ q-form.page-shell(@submit.prevent='save' @validation-error='onValidationError' g
|
||||
q-card.q-mt-md(flat)
|
||||
q-card-section.q-py-sm
|
||||
.text-h6 Подписант
|
||||
.text-caption.text-grey-7 Кто подписывает отчёты: председатель или представитель (по доверенности)
|
||||
.text-caption.t-muted Кто подписывает отчёты: председатель или представитель (по доверенности)
|
||||
|
||||
q-separator
|
||||
|
||||
@@ -331,6 +331,14 @@ onMounted(async () => {
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
// Top-level страница сама задаёт канон-отступы (как WalletPage).
|
||||
.settings-page {
|
||||
padding: var(--p-6, 24px);
|
||||
@media (max-width: 768px) {
|
||||
padding: var(--p-4, 16px);
|
||||
}
|
||||
}
|
||||
|
||||
.save-bar {
|
||||
position: sticky;
|
||||
bottom: 0;
|
||||
@@ -339,24 +347,19 @@ onMounted(async () => {
|
||||
z-index: 10;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-top: 16px;
|
||||
padding: 12px 16px;
|
||||
background: #fff;
|
||||
border-top: 1px solid rgba(0, 0, 0, 0.08);
|
||||
box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.04);
|
||||
gap: var(--p-3, 12px);
|
||||
margin-top: var(--p-4, 16px);
|
||||
padding: var(--p-3, 12px) var(--p-4, 16px);
|
||||
background: var(--p-surface);
|
||||
border-top: 1px solid var(--p-line);
|
||||
border-radius: var(--p-r-md, 12px) var(--p-r-md, 12px) 0 0;
|
||||
}
|
||||
|
||||
.save-bar-status {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 13px;
|
||||
color: #2e7d32;
|
||||
}
|
||||
|
||||
.body--dark .save-bar {
|
||||
background: #1d1d1d;
|
||||
border-top-color: rgba(255, 255, 255, 0.08);
|
||||
font-size: var(--p-fs-body-sm, 13px);
|
||||
color: var(--p-pos);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template lang="pug">
|
||||
div.page-shell
|
||||
q-card.q-mt-md(flat)
|
||||
q-card(flat)
|
||||
q-table.full-height(
|
||||
flat
|
||||
:grid='isMobile'
|
||||
@@ -26,10 +26,9 @@ div.page-shell
|
||||
WalletIdCell(:wallet-name='props.row.id')
|
||||
q-td {{ props.row.name }}
|
||||
q-td.text-right {{ formatAsset2Digits(props.row.available) }}
|
||||
q-td.text-right {{ formatAsset2Digits(props.row.blocked) }}
|
||||
q-td.text-right(auto-width)
|
||||
q-btn(
|
||||
v-if='isChairman'
|
||||
v-if='isChairman && transferEnabled'
|
||||
flat dense round size='sm' color='primary'
|
||||
icon='fa-solid fa-arrow-right-arrow-left'
|
||||
@click='openTransferFor(props.row.id)'
|
||||
@@ -114,9 +113,6 @@ div.page-shell
|
||||
.col-6
|
||||
.text-caption.caption-muted Доступно
|
||||
.text-body2.text-weight-medium {{ formatAsset2Digits(props.row.available) }}
|
||||
.col-6
|
||||
.text-caption.caption-muted Заблокировано
|
||||
.text-body2.text-weight-medium {{ formatAsset2Digits(props.row.blocked) }}
|
||||
|
||||
WalletTransferDialog(
|
||||
v-model='transferDialog.open'
|
||||
@@ -149,6 +145,13 @@ const ledger2Store = useLedger2Store()
|
||||
const session = useSessionStore()
|
||||
const { isChairman } = storeToRefs(session)
|
||||
|
||||
// Переводы между кошельками временно скрыты. Текущий UI открывал перевод
|
||||
// с любого кошелька, включая L3-кошельки пайщиков (w.wal.member) — это
|
||||
// неверно: переводы должны быть доступны только между кооперативными
|
||||
// кошельками. Вернём, когда определим допустимые пары источник→назначение.
|
||||
// Весь код перевода (диалог, обработчики, колонка) сохранён под этим флагом.
|
||||
const transferEnabled = false
|
||||
|
||||
const { registerAction, unregisterAction } = useHeaderActions()
|
||||
const HEADER_ACTION_ID = 'reports-wallets-coop-transfer'
|
||||
|
||||
@@ -208,9 +211,8 @@ const columns = computed<any[]>(() => {
|
||||
{ name: 'id', align: 'left', label: 'ID', field: 'id', sortable: true },
|
||||
{ name: 'name', align: 'left', label: 'Наименование', field: 'name', sortable: true },
|
||||
{ name: 'available', align: 'right', label: 'Доступно', field: 'available', sortable: true },
|
||||
{ name: 'blocked', align: 'right', label: 'Заблокировано', field: 'blocked', sortable: true },
|
||||
]
|
||||
if (isChairman.value) {
|
||||
if (isChairman.value && transferEnabled) {
|
||||
base.push({ name: 'actions', align: 'right', label: '', field: 'actions', sortable: false })
|
||||
}
|
||||
return base
|
||||
@@ -273,7 +275,8 @@ onMounted(async () => {
|
||||
|
||||
// Кнопка «Перевести» в шапке сайта — только председателю.
|
||||
// Регистрируем после загрузки, чтобы не мигала на ходу.
|
||||
if (isChairman.value) {
|
||||
// transferEnabled=false — переводы временно скрыты целиком (см. выше).
|
||||
if (isChairman.value && transferEnabled) {
|
||||
registerAction({
|
||||
id: HEADER_ACTION_ID,
|
||||
component: markRaw(TransferWalletsButton),
|
||||
@@ -298,11 +301,8 @@ onBeforeUnmount(() => {
|
||||
white-space: normal;
|
||||
word-break: break-word;
|
||||
}
|
||||
// Приглушённый caption-цвет, согласованный с темой. Не использовать
|
||||
// quasar `text-grey-6` — он не реагирует на body--dark и плохо читается
|
||||
// на тёмной теме.
|
||||
// Канон-токен --p-ink-2 сам адаптируется к тёмной теме.
|
||||
.caption-muted {
|
||||
color: rgba(0, 0, 0, 0.6);
|
||||
.body--dark & { color: rgba(255, 255, 255, 0.6); }
|
||||
color: var(--p-ink-2);
|
||||
}
|
||||
</style>
|
||||
|
||||
+27
-55
@@ -1,6 +1,6 @@
|
||||
<template lang="pug">
|
||||
div.page-shell
|
||||
q-card.q-mt-md(flat)
|
||||
q-card(flat)
|
||||
q-table.full-height.pw-table(
|
||||
flat
|
||||
:grid='isMobile'
|
||||
@@ -115,27 +115,25 @@ function programTypeFor(colName: string): string {
|
||||
|
||||
function totalFor(username: string): IWalletCell {
|
||||
const row = data.value.matrix[username]
|
||||
if (!row) return { available: 0, blocked: 0 }
|
||||
let a = 0, b = 0
|
||||
if (!row) return { available: 0 }
|
||||
let a = 0
|
||||
for (const c of Object.values(row)) {
|
||||
a += c.available
|
||||
b += c.blocked
|
||||
}
|
||||
return { available: a, blocked: b }
|
||||
return { available: a }
|
||||
}
|
||||
|
||||
const grandTotal = computed<IWalletCell>(() => {
|
||||
let a = 0, b = 0
|
||||
let a = 0
|
||||
for (const c of Object.values(data.value.totals)) {
|
||||
a += c.available
|
||||
b += c.blocked
|
||||
}
|
||||
return { available: a, blocked: b }
|
||||
return { available: a }
|
||||
})
|
||||
|
||||
function cellClass(cell?: IWalletCell): string {
|
||||
if (!cell) return 'cell-empty'
|
||||
if (cell.available === 0 && cell.blocked === 0) return 'cell-zero'
|
||||
if (cell.available === 0) return 'cell-zero'
|
||||
return 'cell-has-value'
|
||||
}
|
||||
|
||||
@@ -149,7 +147,7 @@ const columns = computed(() => {
|
||||
label: prog.title,
|
||||
field: (row: IAccount) => {
|
||||
const c = data.value.matrix[row.username]?.[prog.id]
|
||||
return c ? c.available + c.blocked : 0
|
||||
return c ? c.available : 0
|
||||
},
|
||||
sortable: true,
|
||||
}))
|
||||
@@ -168,7 +166,7 @@ const columns = computed(() => {
|
||||
label: 'Итого',
|
||||
field: (row: IAccount) => {
|
||||
const t = totalFor(row.username)
|
||||
return t.available + t.blocked
|
||||
return t.available
|
||||
},
|
||||
sortable: true,
|
||||
},
|
||||
@@ -182,9 +180,8 @@ const columns = computed(() => {
|
||||
]
|
||||
})
|
||||
|
||||
// Inline-рендер ячейки «available / blocked с иконками». Вынесен в h-function,
|
||||
// чтобы не таскать ещё одну SFC. Иконки: lock-open / lock + q-tooltip — чтобы
|
||||
// без подписей было понятно где «доступно», где «заблокировано».
|
||||
// Inline-рендер ячейки «доступно». Вынесен в h-function, чтобы не таскать ещё
|
||||
// одну SFC. Иконка lock-open + q-tooltip «Доступно».
|
||||
const WalletCell = {
|
||||
name: 'WalletCell',
|
||||
props: {
|
||||
@@ -197,26 +194,17 @@ const WalletCell = {
|
||||
if (!c) {
|
||||
return h('div', { class: 'cell-dash' }, '—')
|
||||
}
|
||||
const renderLine = (
|
||||
amount: number,
|
||||
kind: 'avail' | 'blocked',
|
||||
) => {
|
||||
const valueClass = amount > 0 ? `value-${kind}` : 'value-zero'
|
||||
const tooltip = kind === 'avail' ? 'Доступно' : 'Заблокировано'
|
||||
const icon = kind === 'avail' ? 'fa-solid fa-lock-open' : 'fa-solid fa-lock'
|
||||
return h(
|
||||
const valueClass = c.available > 0 ? 'value-avail' : 'value-zero'
|
||||
return h('div', { class: 'wallet-cell' }, [
|
||||
h(
|
||||
'div',
|
||||
{ class: ['cell-line', valueClass, props.bold ? 'bold' : ''].join(' ') },
|
||||
[
|
||||
h(QIcon, { name: icon, size: '12px', class: 'cell-icon' }),
|
||||
h('span', { class: 'cell-value' }, formatAsset2Digits(`${amount} RUB`)),
|
||||
h(QTooltip, { anchor: 'top middle', self: 'bottom middle', delay: 200 }, () => tooltip),
|
||||
h(QIcon, { name: 'fa-solid fa-lock-open', size: '12px', class: 'cell-icon' }),
|
||||
h('span', { class: 'cell-value' }, formatAsset2Digits(`${c.available} RUB`)),
|
||||
h(QTooltip, { anchor: 'top middle', self: 'bottom middle', delay: 200 }, () => 'Доступно'),
|
||||
],
|
||||
)
|
||||
}
|
||||
return h('div', { class: 'wallet-cell' }, [
|
||||
renderLine(c.available, 'avail'),
|
||||
renderLine(c.blocked, 'blocked'),
|
||||
),
|
||||
])
|
||||
}
|
||||
},
|
||||
@@ -251,12 +239,9 @@ onMounted(() => void reload())
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
// Все цвета через quasar runtime variables (`var(--q-*)`) и body--dark
|
||||
// overrides — без хардкода hex. Иначе светлая/тёмная темы выглядят плохо.
|
||||
// Не использовать quasar `text-grey-6` — он не реагирует на body--dark.
|
||||
// Канон-токены сами адаптируются к тёмной теме — без rgba-хардкода.
|
||||
.caption-muted {
|
||||
color: rgba(0, 0, 0, 0.6);
|
||||
.body--dark & { color: rgba(255, 255, 255, 0.6); }
|
||||
color: var(--p-ink-2);
|
||||
}
|
||||
|
||||
.pw-table {
|
||||
@@ -277,19 +262,11 @@ onMounted(() => void reload())
|
||||
}
|
||||
|
||||
:deep(.row-totals) {
|
||||
background: rgba(0, 0, 0, 0.04);
|
||||
background: var(--p-surface-2);
|
||||
font-weight: 600;
|
||||
|
||||
td {
|
||||
border-top: 2px solid rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
.body--dark & {
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
|
||||
td {
|
||||
border-top-color: rgba(255, 255, 255, 0.12);
|
||||
}
|
||||
border-top: 2px solid var(--p-line-2);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -315,7 +292,7 @@ onMounted(() => void reload())
|
||||
</style>
|
||||
|
||||
<!--
|
||||
«available + blocked» рендерятся через render-функцию `WalletCell`,
|
||||
«available» рендерится через render-функцию `WalletCell`,
|
||||
у которой нет своего scoped-id. scoped + :deep до неё не доходит
|
||||
стабильно, поэтому стили лежат в global-блоке. Класс `.wallet-cell`
|
||||
достаточно специфичен — конфликтов с другими компонентами нет.
|
||||
@@ -336,10 +313,10 @@ onMounted(() => void reload())
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
|
||||
&.value-avail { color: var(--q-positive); }
|
||||
&.value-blocked { color: var(--q-warning); }
|
||||
&.value-avail { color: var(--p-pos); }
|
||||
&.value-blocked { color: var(--p-warn); }
|
||||
&.value-zero {
|
||||
color: rgba(0, 0, 0, 0.55);
|
||||
color: var(--p-ink-3);
|
||||
font-weight: 400;
|
||||
}
|
||||
&.bold { font-weight: 700; }
|
||||
@@ -351,12 +328,7 @@ onMounted(() => void reload())
|
||||
}
|
||||
|
||||
.cell-dash {
|
||||
color: rgba(0, 0, 0, 0.55);
|
||||
color: var(--p-ink-3);
|
||||
}
|
||||
}
|
||||
|
||||
.body--dark .wallet-cell {
|
||||
.cell-line.value-zero { color: rgba(255, 255, 255, 0.7); }
|
||||
.cell-dash { color: rgba(255, 255, 255, 0.7); }
|
||||
}
|
||||
</style>
|
||||
|
||||
+6
-5
@@ -1,12 +1,13 @@
|
||||
<template lang="pug">
|
||||
q-btn(
|
||||
size='md'
|
||||
flat
|
||||
color='secondary'
|
||||
icon='fa-solid fa-arrow-right-arrow-left'
|
||||
label='Перевести'
|
||||
color='primary'
|
||||
no-caps
|
||||
no-wrap
|
||||
unelevated
|
||||
@click='onClick'
|
||||
)
|
||||
q-icon.q-mr-sm(name='fa-solid fa-arrow-right-arrow-left' size='15px')
|
||||
span Перевести
|
||||
q-tooltip Корректировка: перевод между кошельками одного бух.счёта
|
||||
</template>
|
||||
|
||||
|
||||
+89
-85
@@ -1,95 +1,87 @@
|
||||
<template lang="pug">
|
||||
q-dialog(
|
||||
BaseDialog(
|
||||
:model-value='modelValue'
|
||||
@update:model-value='emit("update:modelValue", $event)'
|
||||
persistent
|
||||
size='lg'
|
||||
title='Перевод между кошельками'
|
||||
:close-on-backdrop='!loading'
|
||||
:close-on-escape='!loading'
|
||||
)
|
||||
q-card(style='min-width: 480px; max-width: 540px;')
|
||||
q-bar.bg-primary.text-white
|
||||
.text-subtitle1 Перевод между кошельками
|
||||
q-space
|
||||
q-btn(flat dense icon='fa-solid fa-xmark' @click='close')
|
||||
q-tooltip Закрыть
|
||||
p.transfer-dialog__lead
|
||||
| Перевод между кошельками одного бухгалтерского счёта без изменения сальдо счёта.
|
||||
| Доступен только председателю.
|
||||
|
||||
q-card-section.q-pa-md
|
||||
.text-caption.caption-muted.q-mb-md
|
||||
| Перевод между кошельками одного бухгалтерского счёта без изменения сальдо счёта.
|
||||
| Доступен только председателю.
|
||||
q-form(@submit.prevent='submit' ref='formRef' greedy)
|
||||
q-select.q-mb-sm(
|
||||
v-model='form.fromWallet'
|
||||
:options='fromOptions'
|
||||
option-label='label'
|
||||
option-value='id'
|
||||
emit-value map-options
|
||||
label='Из кошелька'
|
||||
outlined dense
|
||||
:disable='!!props.fixedFromWallet'
|
||||
:rules='[(v) => !!v || "Выберите кошелёк-источник"]'
|
||||
@update:model-value='onFromChange'
|
||||
)
|
||||
template(#option='scope')
|
||||
q-item(v-bind='scope.itemProps')
|
||||
q-item-section
|
||||
q-item-label {{ scope.opt.label }}
|
||||
q-item-label(caption) Доступно: {{ scope.opt.available }}
|
||||
|
||||
q-form(@submit.prevent='submit' ref='formRef' greedy)
|
||||
q-select.q-mb-sm(
|
||||
v-model='form.fromWallet'
|
||||
:options='fromOptions'
|
||||
option-label='label'
|
||||
option-value='id'
|
||||
emit-value map-options
|
||||
label='Из кошелька'
|
||||
outlined dense
|
||||
:disable='!!props.fixedFromWallet'
|
||||
:rules='[(v) => !!v || "Выберите кошелёк-источник"]'
|
||||
@update:model-value='onFromChange'
|
||||
)
|
||||
template(#option='scope')
|
||||
q-item(v-bind='scope.itemProps')
|
||||
q-item-section
|
||||
q-item-label {{ scope.opt.label }}
|
||||
q-item-label(caption) Доступно: {{ scope.opt.available }}
|
||||
q-select.q-mb-sm(
|
||||
v-model='form.toWallet'
|
||||
:options='toOptions'
|
||||
option-label='label'
|
||||
option-value='id'
|
||||
emit-value map-options
|
||||
label='В кошелёк'
|
||||
outlined dense
|
||||
:disable='!form.fromWallet'
|
||||
:hint='toHint'
|
||||
:rules='[(v) => !!v || "Выберите кошелёк-получатель", (v) => v !== form.fromWallet || "Источник и получатель не должны совпадать"]'
|
||||
)
|
||||
template(#option='scope')
|
||||
q-item(v-bind='scope.itemProps')
|
||||
q-item-section
|
||||
q-item-label {{ scope.opt.label }}
|
||||
q-item-label(caption) Доступно: {{ scope.opt.available }}
|
||||
//- Причина пустого списка важнее, чем «No results» по дефолту:
|
||||
//- walmove ходит ТОЛЬКО внутри одного бух.счёта, поэтому если у
|
||||
//- кооператива нет других кошельков на том же счёте — выбора нет.
|
||||
template(#no-option)
|
||||
q-item
|
||||
q-item-section.text-italic.caption-muted
|
||||
| Нет других кошельков на бух.счёте {{ accountIdLabel }}.
|
||||
| Перевод между разными счетами требует решения совета.
|
||||
|
||||
q-select.q-mb-sm(
|
||||
v-model='form.toWallet'
|
||||
:options='toOptions'
|
||||
option-label='label'
|
||||
option-value='id'
|
||||
emit-value map-options
|
||||
label='В кошелёк'
|
||||
outlined dense
|
||||
:disable='!form.fromWallet'
|
||||
:hint='toHint'
|
||||
:rules='[(v) => !!v || "Выберите кошелёк-получатель", (v) => v !== form.fromWallet || "Источник и получатель не должны совпадать"]'
|
||||
)
|
||||
template(#option='scope')
|
||||
q-item(v-bind='scope.itemProps')
|
||||
q-item-section
|
||||
q-item-label {{ scope.opt.label }}
|
||||
q-item-label(caption) Доступно: {{ scope.opt.available }}
|
||||
//- Причина пустого списка важнее, чем «No results» по дефолту:
|
||||
//- walmove ходит ТОЛЬКО внутри одного бух.счёта, поэтому если у
|
||||
//- кооператива нет других кошельков на том же счёте — выбора нет.
|
||||
template(#no-option)
|
||||
q-item
|
||||
q-item-section.text-italic.caption-muted
|
||||
| Нет других кошельков на бух.счёте {{ accountIdLabel }}.
|
||||
| Перевод между разными счетами требует решения совета.
|
||||
.row.items-center.q-gutter-sm.q-mb-sm(v-if='form.fromWallet')
|
||||
.col
|
||||
.text-caption.caption-muted Бух.счёт
|
||||
.text-body2.text-weight-medium {{ accountIdLabel }}
|
||||
|
||||
.row.items-center.q-gutter-sm.q-mb-sm(v-if='form.fromWallet')
|
||||
.col
|
||||
.text-caption.caption-muted Бух.счёт
|
||||
.text-body2.text-weight-medium {{ accountIdLabel }}
|
||||
q-input.q-mb-sm(
|
||||
v-model='form.amountStr'
|
||||
label='Сумма (RUB)'
|
||||
outlined dense type='number' step='0.0001' min='0.0001'
|
||||
:rules='[validateAmount]'
|
||||
hint='До 4 знаков после запятой'
|
||||
)
|
||||
|
||||
q-input.q-mb-sm(
|
||||
v-model='form.amountStr'
|
||||
label='Сумма (RUB)'
|
||||
outlined dense type='number' step='0.0001' min='0.0001'
|
||||
:rules='[validateAmount]'
|
||||
hint='До 4 знаков после запятой'
|
||||
)
|
||||
q-input.q-mb-sm(
|
||||
v-model='form.memo'
|
||||
label='Обоснование'
|
||||
outlined dense type='textarea' rows='3' counter maxlength='255'
|
||||
:rules='[(v) => (v && v.trim().length > 0) || "Обязательно — укажите причину перевода"]'
|
||||
)
|
||||
|
||||
q-input.q-mb-sm(
|
||||
v-model='form.memo'
|
||||
label='Обоснование'
|
||||
outlined dense type='textarea' rows='3' counter maxlength='255'
|
||||
:rules='[(v) => (v && v.trim().length > 0) || "Обязательно — укажите причину перевода"]'
|
||||
)
|
||||
|
||||
.row.justify-end.q-gutter-sm.q-mt-md
|
||||
q-btn(flat label='Отмена' color='grey-7' :disable='loading' @click='close')
|
||||
q-btn(
|
||||
type='submit'
|
||||
color='primary'
|
||||
label='Перевести'
|
||||
:loading='loading'
|
||||
icon='fa-solid fa-arrow-right-arrow-left'
|
||||
)
|
||||
template(#footer)
|
||||
BaseButton(variant='ghost' :disabled='loading' @click='close') Отмена
|
||||
BaseButton(variant='primary' :loading='loading' @click='submit')
|
||||
template(#icon-left)
|
||||
q-icon.q-mr-xs(name='fa-solid fa-arrow-right-arrow-left' size='15px')
|
||||
| Перевести
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
@@ -98,6 +90,8 @@ import { Ledger2 } from 'cooptypes'
|
||||
import { FailAlert, SuccessAlert } from 'src/shared/api'
|
||||
import { useSystemStore } from 'src/entities/System/model'
|
||||
import { useLedger2Store, type ILedger2Wallet } from 'src/entities/Ledger2'
|
||||
import { BaseDialog } from 'src/shared/ui/base/BaseDialog'
|
||||
import { BaseButton } from 'src/shared/ui/base/BaseButton'
|
||||
|
||||
interface Props {
|
||||
modelValue: boolean
|
||||
@@ -218,6 +212,11 @@ watch(
|
||||
)
|
||||
|
||||
async function submit() {
|
||||
if (loading.value) return
|
||||
// Кнопка «Перевести» теперь в footer диалога (вне q-form), поэтому
|
||||
// валидацию правил полей запускаем явно перед отправкой.
|
||||
const valid = await formRef.value?.validate()
|
||||
if (!valid) return
|
||||
if (!form.fromWallet || !form.toWallet) return
|
||||
if (!sourceAccountId.value) {
|
||||
FailAlert(new Error('Не удалось определить бух.счёт для выбранного кошелька'))
|
||||
@@ -254,9 +253,14 @@ function close() {
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
// Не использовать quasar `text-grey-X` — фиксированный hex без body--dark.
|
||||
.transfer-dialog__lead {
|
||||
margin: 0;
|
||||
color: var(--p-ink-2);
|
||||
font-size: var(--p-fs-body-sm, 13px);
|
||||
line-height: 1.5;
|
||||
}
|
||||
// Канон-токен --p-ink-2 сам адаптируется к тёмной теме — без rgba-хардкода.
|
||||
.caption-muted {
|
||||
color: rgba(0, 0, 0, 0.6);
|
||||
.body--dark & { color: rgba(255, 255, 255, 0.6); }
|
||||
color: var(--p-ink-2);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,44 +1,37 @@
|
||||
<template lang="pug">
|
||||
.column.flex-1.min-h-0.min-w-0.no-wrap
|
||||
router-view
|
||||
.reports-shell
|
||||
SecondLevelTabs(:tabs='tabs')
|
||||
.reports-shell__content
|
||||
router-view
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, markRaw, onBeforeUnmount, onMounted } from 'vue'
|
||||
import { useHeaderActions } from 'src/shared/hooks/useHeaderActions'
|
||||
import RouteMenuButton from 'src/shared/ui/RouteMenuButton/RouteMenuButton.vue'
|
||||
import { SecondLevelTabs } from 'src/shared/ui/SecondLevelTabs'
|
||||
|
||||
// Shell-страница «Кошельки»: инжектит две кнопки в шапку сайта
|
||||
// (Кооператив / Пайщики), переключает через именованные дочерние маршруты.
|
||||
// Паттерн один-в-один как DocumentsPage.
|
||||
const { registerAction, clearActions } = useHeaderActions()
|
||||
|
||||
const menuButtons = computed(() => [
|
||||
{
|
||||
id: 'reports-wallets-coop-menu',
|
||||
component: markRaw(RouteMenuButton),
|
||||
props: {
|
||||
routeName: 'reports-wallets-coop',
|
||||
label: 'Кооператив',
|
||||
},
|
||||
order: 1,
|
||||
},
|
||||
{
|
||||
id: 'reports-wallets-participants-menu',
|
||||
component: markRaw(RouteMenuButton),
|
||||
props: {
|
||||
routeName: 'reports-wallets-participants',
|
||||
label: 'Пайщики',
|
||||
},
|
||||
order: 2,
|
||||
},
|
||||
])
|
||||
|
||||
onMounted(() => {
|
||||
menuButtons.value.forEach((b) => registerAction(b))
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
clearActions()
|
||||
})
|
||||
// Shell-страница «Реестр кошельков»: канон-меню второго уровня
|
||||
// (Кооператив / Пайщики). Кнопка «Перевести» живёт в топбаре —
|
||||
// её регистрирует дочерняя CoopWalletsPage через useHeaderActions.
|
||||
const tabs = [
|
||||
{ routeName: 'reports-wallets-coop', label: 'Кооператив', icon: 'fa-solid fa-building' },
|
||||
{ routeName: 'reports-wallets-participants', label: 'Пайщики', icon: 'fa-solid fa-users' },
|
||||
]
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.reports-shell {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
min-width: 0;
|
||||
}
|
||||
.reports-shell__content {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
padding: var(--p-6, 24px);
|
||||
@media (max-width: 768px) {
|
||||
padding: var(--p-4, 16px);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
+3
-6
@@ -23,12 +23,11 @@ export interface IProgramColumn {
|
||||
|
||||
export interface IWalletCell {
|
||||
available: number
|
||||
blocked: number
|
||||
}
|
||||
|
||||
export interface IProgramsAndWallets {
|
||||
programs: IProgramColumn[]
|
||||
/** matrix[username][program_id] → {available, blocked}. Отсутствие ключа = кошелька нет. */
|
||||
/** matrix[username][program_id] → {available}. Отсутствие ключа = кошелька нет. */
|
||||
matrix: Record<string, Record<number, IWalletCell>>
|
||||
/** Сумма по каждой программе (по всем кошелькам). */
|
||||
totals: Record<number, IWalletCell>
|
||||
@@ -77,7 +76,7 @@ export async function loadProgramsAndWallets(
|
||||
|
||||
const matrix: Record<string, Record<number, IWalletCell>> = {}
|
||||
const totals: Record<number, IWalletCell> = {}
|
||||
for (const p of programs) totals[p.id] = { available: 0, blocked: 0 }
|
||||
for (const p of programs) totals[p.id] = { available: 0 }
|
||||
|
||||
const wallets = walletsResponse[Queries.Wallet.GetProgramWallets.name]?.items ?? []
|
||||
|
||||
@@ -85,13 +84,11 @@ export async function loadProgramsAndWallets(
|
||||
const username = String(w.username)
|
||||
const program_id = Number(w.program_id)
|
||||
const avail = parseAsset(w.available)
|
||||
const blocked = parseAsset(w.blocked)
|
||||
if (!matrix[username]) matrix[username] = {}
|
||||
matrix[username][program_id] = { available: avail, blocked }
|
||||
matrix[username][program_id] = { available: avail }
|
||||
const t = totals[program_id]
|
||||
if (t) {
|
||||
t.available += avail
|
||||
t.blocked += blocked
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template lang="pug">
|
||||
span(v-if='accountCode === null || accountCode === undefined').text-grey-6 —
|
||||
span(v-if='accountCode === null || accountCode === undefined').t-faint —
|
||||
span.account-id-cell(v-else)
|
||||
EntityIdBadge(
|
||||
:rawId='accountCode'
|
||||
|
||||
@@ -12,10 +12,9 @@
|
||||
color='negative'
|
||||
size='xs'
|
||||
)
|
||||
q-icon(
|
||||
q-icon.direction-cell__move(
|
||||
v-else
|
||||
name='fa-solid fa-right-left'
|
||||
color='grey-6'
|
||||
size='xs'
|
||||
)
|
||||
span.text-caption {{ label }}
|
||||
@@ -34,3 +33,9 @@ const label = computed(() => {
|
||||
return 'Перевод'
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.direction-cell__move {
|
||||
color: var(--p-ink-3);
|
||||
}
|
||||
</style>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user