[C28-38][@ant] фича: UI приватных КУ — управление белым списком + фильтр выбора
SDK: операции setBranchPrivate/addBranchWhitelist/deleteBranchWhitelist. desktop: на странице КУ председателю — разворот строки с галочкой «Приватный» и белым списком (добавление по ФИО/удаление); значок «Приватный» в списке; фильтр недоступных приватных участков (is_available) при регистрации и смене КУ. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,34 @@
|
||||
import type { IBranch } from 'src/entities/Branch/model'
|
||||
import type { IAddBranchWhitelistInput, IDeleteBranchWhitelistInput, ISetBranchPrivateInput } from '../model'
|
||||
import { client } from 'src/shared/api/client'
|
||||
import { Mutations } from '@coopenomics/sdk'
|
||||
|
||||
async function setBranchPrivate(data: ISetBranchPrivateInput): Promise<IBranch> {
|
||||
const { [Mutations.Branches.SetBranchPrivate.name]: result } = await client.Mutation(
|
||||
Mutations.Branches.SetBranchPrivate.mutation,
|
||||
{ variables: { data } }
|
||||
)
|
||||
return result
|
||||
}
|
||||
|
||||
async function addBranchWhitelist(data: IAddBranchWhitelistInput): Promise<IBranch> {
|
||||
const { [Mutations.Branches.AddBranchWhitelist.name]: result } = await client.Mutation(
|
||||
Mutations.Branches.AddBranchWhitelist.mutation,
|
||||
{ variables: { data } }
|
||||
)
|
||||
return result
|
||||
}
|
||||
|
||||
async function deleteBranchWhitelist(data: IDeleteBranchWhitelistInput): Promise<IBranch> {
|
||||
const { [Mutations.Branches.DeleteBranchWhitelist.name]: result } = await client.Mutation(
|
||||
Mutations.Branches.DeleteBranchWhitelist.mutation,
|
||||
{ variables: { data } }
|
||||
)
|
||||
return result
|
||||
}
|
||||
|
||||
export const api = {
|
||||
setBranchPrivate,
|
||||
addBranchWhitelist,
|
||||
deleteBranchWhitelist,
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from './model'
|
||||
@@ -0,0 +1,33 @@
|
||||
import type { Mutations } from '@coopenomics/sdk'
|
||||
import { api } from '../api'
|
||||
import { useBranchStore, type IBranch } from 'src/entities/Branch/model'
|
||||
import { useSystemStore } from 'src/entities/System/model'
|
||||
|
||||
export type ISetBranchPrivateInput = Mutations.Branches.SetBranchPrivate.IInput['data']
|
||||
export type IAddBranchWhitelistInput = Mutations.Branches.AddBranchWhitelist.IInput['data']
|
||||
export type IDeleteBranchWhitelistInput = Mutations.Branches.DeleteBranchWhitelist.IInput['data']
|
||||
|
||||
export function useBranchPrivacy() {
|
||||
const store = useBranchStore()
|
||||
const { info } = useSystemStore()
|
||||
|
||||
async function setBranchPrivate(data: ISetBranchPrivateInput): Promise<IBranch> {
|
||||
const branch = await api.setBranchPrivate(data)
|
||||
await store.loadBranches({ coopname: info.coopname })
|
||||
return branch
|
||||
}
|
||||
|
||||
async function addBranchWhitelist(data: IAddBranchWhitelistInput): Promise<IBranch> {
|
||||
const branch = await api.addBranchWhitelist(data)
|
||||
await store.loadBranches({ coopname: info.coopname })
|
||||
return branch
|
||||
}
|
||||
|
||||
async function deleteBranchWhitelist(data: IDeleteBranchWhitelistInput): Promise<IBranch> {
|
||||
const branch = await api.deleteBranchWhitelist(data)
|
||||
await store.loadBranches({ coopname: info.coopname })
|
||||
return branch
|
||||
}
|
||||
|
||||
return { setBranchPrivate, addBranchWhitelist, deleteBranchWhitelist }
|
||||
}
|
||||
@@ -25,33 +25,80 @@
|
||||
table.table
|
||||
thead
|
||||
tr
|
||||
th.col-expand(v-if='isChairman')
|
||||
th Участок
|
||||
th Председатель
|
||||
th Доверенные
|
||||
th.col-count Пайщики
|
||||
th.col-action
|
||||
tbody
|
||||
tr.data-row(
|
||||
v-for='branch in branches',
|
||||
:key='branch.braname',
|
||||
@click='openDetails(branch.braname)'
|
||||
)
|
||||
td
|
||||
.doc-primary {{ branchTitle(branch) }}
|
||||
.t-sm.t-muted(v-if='branch.fact_address') {{ branch.fact_address }}
|
||||
td {{ chairmanName(branch) }}
|
||||
td
|
||||
template(v-if='trustedNames(branch).length')
|
||||
div(v-for='name in trustedNames(branch)', :key='name') {{ name }}
|
||||
span(v-else) —
|
||||
td.col-count {{ branch.participants_count ?? 0 }}
|
||||
td.col-action
|
||||
button.icon-btn(
|
||||
type='button',
|
||||
aria-label='Открыть участок',
|
||||
@click.stop='openDetails(branch.braname)'
|
||||
)
|
||||
q-icon(name='chevron_right')
|
||||
template(v-for='branch in branches', :key='branch.braname')
|
||||
tr.data-row(@click='openDetails(branch.braname)')
|
||||
td.col-expand(v-if='isChairman', @click.stop)
|
||||
ExpandToggleButton(
|
||||
variant='card',
|
||||
:expanded='isExpanded(branch.braname)',
|
||||
@click='toggleExpand(branch.braname)'
|
||||
)
|
||||
td
|
||||
.doc-primary
|
||||
span {{ branchTitle(branch) }}
|
||||
BaseChip.q-ml-sm(v-if='branch.is_private', variant='warn')
|
||||
q-icon(name='lock', size='12px')
|
||||
span.q-ml-xs Приватный
|
||||
.t-sm.t-muted(v-if='branch.fact_address') {{ branch.fact_address }}
|
||||
td {{ chairmanName(branch) }}
|
||||
td
|
||||
template(v-if='trustedNames(branch).length')
|
||||
div(v-for='name in trustedNames(branch)', :key='name') {{ name }}
|
||||
span(v-else) —
|
||||
td.col-count {{ branch.participants_count ?? 0 }}
|
||||
td.col-action
|
||||
button.icon-btn(
|
||||
type='button',
|
||||
aria-label='Открыть участок',
|
||||
@click.stop='openDetails(branch.braname)'
|
||||
)
|
||||
q-icon(name='chevron_right')
|
||||
|
||||
//- Панель управления приватностью и белым списком — только председателю совета
|
||||
tr.expand-row(v-if='isChairman && isExpanded(branch.braname)')
|
||||
td(:colspan='6')
|
||||
.branch-admin
|
||||
BaseCheckbox(
|
||||
:model-value='branch.is_private',
|
||||
:disabled='isBusy(branch.braname)',
|
||||
label='Приватный участок — выбрать его при вступлении или смене может только пайщик из белого списка',
|
||||
@update:model-value='onTogglePrivate(branch, $event)'
|
||||
)
|
||||
template(v-if='branch.is_private')
|
||||
.branch-admin__subtitle.t-sm.t-muted Белый список пайщиков
|
||||
.branch-admin__add
|
||||
UserSearchSelector.branch-admin__search(
|
||||
v-model='whitelistInput[branch.braname]',
|
||||
label='Начните ввод ФИО пайщика',
|
||||
dense
|
||||
)
|
||||
BaseButton(
|
||||
variant='primary',
|
||||
:disabled='!whitelistInput[branch.braname]',
|
||||
:loading='isBusy(branch.braname)',
|
||||
@click='onAddWhitelist(branch)'
|
||||
) Добавить
|
||||
.branch-admin__list(v-if='whitelistOf(branch).length')
|
||||
.branch-admin__member(
|
||||
v-for='member in whitelistOf(branch)',
|
||||
:key='member.username'
|
||||
)
|
||||
span {{ certificateName(member) }}
|
||||
button.icon-btn(
|
||||
type='button',
|
||||
aria-label='Удалить из белого списка',
|
||||
:disabled='isBusy(branch.braname)',
|
||||
@click='onRemoveWhitelist(branch, member.username)'
|
||||
)
|
||||
q-icon(name='close')
|
||||
.t-sm.t-muted(v-else) Белый список пуст — добавьте пайщиков, которым разрешён выбор этого участка.
|
||||
EmptyState(
|
||||
v-else,
|
||||
title='Кооперативных участков пока нет',
|
||||
@@ -62,14 +109,19 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
import { computed, onMounted, reactive, ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useBranchStore } from 'src/entities/Branch/model';
|
||||
import { useBranchStore, type IBranch, type IPublicBranch } from 'src/entities/Branch/model';
|
||||
import { useSystemStore } from 'src/entities/System/model';
|
||||
import { useSessionStore } from 'src/entities/Session';
|
||||
import { useBranchPrivacy } from 'src/features/Branch/BranchPrivacy';
|
||||
import { useDismissibleBanner } from 'src/shared/hooks/useDismissibleBanner';
|
||||
import { FailAlert } from 'src/shared/api';
|
||||
import { BaseCard, BaseChip, EmptyState, TableSkeleton } from 'src/shared/ui/base';
|
||||
import { FailAlert, SuccessAlert } from 'src/shared/api';
|
||||
import { BaseButton, BaseCard, BaseCheckbox, BaseChip, EmptyState, TableSkeleton } from 'src/shared/ui/base';
|
||||
import type { TableSkeletonColumn } from 'src/shared/ui/base';
|
||||
import { ExpandToggleButton, UserSearchSelector } from 'src/shared/ui';
|
||||
|
||||
type AnyBranch = IBranch | IPublicBranch;
|
||||
|
||||
const skeletonColumns: TableSkeletonColumn[] = [
|
||||
{ label: 'Участок' },
|
||||
@@ -82,12 +134,18 @@ const skeletonColumns: TableSkeletonColumn[] = [
|
||||
const router = useRouter();
|
||||
const branchStore = useBranchStore();
|
||||
const system = useSystemStore();
|
||||
const session = useSessionStore();
|
||||
const { setBranchPrivate, addBranchWhitelist, deleteBranchWhitelist } = useBranchPrivacy();
|
||||
const { dismissed, dismiss } = useDismissibleBanner('ku:branches:banner-dismissed');
|
||||
|
||||
const loading = ref(true);
|
||||
|
||||
// публичные данные участков (сертификаты с ФИО) — доступны любому пайщику
|
||||
const branches = computed(() => branchStore.publicBranches);
|
||||
// председателю совета доступно управление приватностью — для него грузим полный список
|
||||
// участков (с белым списком ФИО); остальным достаточно публичных данных
|
||||
const isChairman = computed(() => session.isChairman);
|
||||
const branches = computed<AnyBranch[]>(() =>
|
||||
isChairman.value ? branchStore.branches : branchStore.publicBranches,
|
||||
);
|
||||
|
||||
// состояние системы управления из контракта: is_branched отдаётся системным стором
|
||||
// (system.info.cooperator_account). Контракт включает флаг при 3+ кооперативных участках.
|
||||
@@ -98,7 +156,27 @@ const branchModeText = computed(() =>
|
||||
: 'Пока в кооперативе меньше трёх кооперативных участков — все пайщики участвуют в общем собрании напрямую. Когда участков станет три и более, система управления автоматически переключится на мажоритарную: собрания будут проходить через кооперативные участки.',
|
||||
);
|
||||
|
||||
function branchTitle(branch: any): string {
|
||||
// локальное состояние разворота строк и формы белого списка (per-участок)
|
||||
const expanded = reactive(new Map<string, boolean>());
|
||||
const busy = reactive(new Map<string, boolean>());
|
||||
const whitelistInput = reactive<Record<string, string>>({});
|
||||
|
||||
function isExpanded(braname: string): boolean {
|
||||
return expanded.get(braname) ?? false;
|
||||
}
|
||||
function toggleExpand(braname: string) {
|
||||
expanded.set(braname, !isExpanded(braname));
|
||||
}
|
||||
function isBusy(braname: string): boolean {
|
||||
return busy.get(braname) ?? false;
|
||||
}
|
||||
|
||||
// белый список (ФИО) есть только в полном объекте участка (доступен председателю)
|
||||
function whitelistOf(branch: AnyBranch) {
|
||||
return 'whitelist_certificates' in branch ? branch.whitelist_certificates : [];
|
||||
}
|
||||
|
||||
function branchTitle(branch: AnyBranch): string {
|
||||
return branch.short_name || branch.full_name || branch.braname;
|
||||
}
|
||||
|
||||
@@ -110,11 +188,11 @@ function certificateName(certificate: any): string {
|
||||
);
|
||||
}
|
||||
|
||||
function chairmanName(branch: any): string {
|
||||
function chairmanName(branch: AnyBranch): string {
|
||||
return certificateName(branch.trustee_certificate);
|
||||
}
|
||||
|
||||
function trustedNames(branch: any): string[] {
|
||||
function trustedNames(branch: AnyBranch): string[] {
|
||||
return (branch.trusted_certificates ?? []).map((certificate: any) => certificateName(certificate));
|
||||
}
|
||||
|
||||
@@ -122,10 +200,53 @@ function openDetails(braname: string) {
|
||||
router.push({ name: 'ku-branch-details', params: { coopname: system.info.coopname, braname } });
|
||||
}
|
||||
|
||||
async function onTogglePrivate(branch: AnyBranch, value: boolean) {
|
||||
busy.set(branch.braname, true);
|
||||
try {
|
||||
await setBranchPrivate({ coopname: system.info.coopname, braname: branch.braname, is_private: value });
|
||||
SuccessAlert(value ? 'Участок сделан приватным' : 'Участок сделан публичным');
|
||||
} catch (e: unknown) {
|
||||
FailAlert(e);
|
||||
} finally {
|
||||
busy.set(branch.braname, false);
|
||||
}
|
||||
}
|
||||
|
||||
async function onAddWhitelist(branch: AnyBranch) {
|
||||
const account = whitelistInput[branch.braname];
|
||||
if (!account) return;
|
||||
busy.set(branch.braname, true);
|
||||
try {
|
||||
await addBranchWhitelist({ coopname: system.info.coopname, braname: branch.braname, account });
|
||||
whitelistInput[branch.braname] = '';
|
||||
SuccessAlert('Пайщик добавлен в белый список участка');
|
||||
} catch (e: unknown) {
|
||||
FailAlert(e);
|
||||
} finally {
|
||||
busy.set(branch.braname, false);
|
||||
}
|
||||
}
|
||||
|
||||
async function onRemoveWhitelist(branch: AnyBranch, account: string) {
|
||||
busy.set(branch.braname, true);
|
||||
try {
|
||||
await deleteBranchWhitelist({ coopname: system.info.coopname, braname: branch.braname, account });
|
||||
SuccessAlert('Пайщик удалён из белого списка участка');
|
||||
} catch (e: unknown) {
|
||||
FailAlert(e);
|
||||
} finally {
|
||||
busy.set(branch.braname, false);
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
await branchStore.loadPublicBranches({ coopname: system.info.coopname });
|
||||
if (isChairman.value) {
|
||||
await branchStore.loadBranches({ coopname: system.info.coopname });
|
||||
} else {
|
||||
await branchStore.loadPublicBranches({ coopname: system.info.coopname });
|
||||
}
|
||||
} catch (e: unknown) {
|
||||
FailAlert(e);
|
||||
} finally {
|
||||
@@ -145,4 +266,55 @@ onMounted(async () => {
|
||||
width: 1%;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* колонка со стрелкой разворота — узкая */
|
||||
.col-expand {
|
||||
width: 1%;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* строка управления приватностью под участком */
|
||||
.expand-row > td {
|
||||
background: var(--p-surface-2);
|
||||
padding: var(--p-4);
|
||||
}
|
||||
|
||||
.branch-admin {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--p-3);
|
||||
}
|
||||
|
||||
.branch-admin__subtitle {
|
||||
margin-top: var(--p-2);
|
||||
}
|
||||
|
||||
.branch-admin__add {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: var(--p-3);
|
||||
}
|
||||
|
||||
.branch-admin__search {
|
||||
flex: 1 1 auto;
|
||||
max-width: 420px;
|
||||
}
|
||||
|
||||
.branch-admin__list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--p-2);
|
||||
max-width: 420px;
|
||||
}
|
||||
|
||||
.branch-admin__member {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--p-2);
|
||||
padding: var(--p-2) var(--p-3);
|
||||
background: var(--p-surface-1);
|
||||
border: 1px solid var(--p-line);
|
||||
border-radius: var(--p-r-sm);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -55,7 +55,7 @@ watch(() => store.state.step, async () => {
|
||||
load()
|
||||
}, { immediate: true })
|
||||
|
||||
// Массив используется без изменений
|
||||
const branches = computed(() => branchStore.publicBranches)
|
||||
// приватные участки, недоступные текущему пайщику (не в белом списке), к выбору не показываем
|
||||
const branches = computed(() => branchStore.publicBranches.filter((branch) => branch.is_available))
|
||||
|
||||
</script>
|
||||
|
||||
@@ -22,7 +22,8 @@ export function useSelectBranchProcess() {
|
||||
const branchStore = useBranchStore()
|
||||
const { selectBranch } = useSelectBranch()
|
||||
|
||||
const branches = computed(() => branchStore.publicBranches)
|
||||
// приватные участки, недоступные текущему пайщику (не в белом списке), к выбору не показываем
|
||||
const branches = computed(() => branchStore.publicBranches.filter((branch) => branch.is_available))
|
||||
|
||||
// Вотчер на авторизацию
|
||||
watch(
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import { type branchModel, branchSelector } from '../../selectors'
|
||||
import { $, type GraphQLTypes, type InputType, type ModelTypes, Selector } from '../../zeus/index'
|
||||
|
||||
export const name = 'addBranchWhitelist'
|
||||
|
||||
export const mutation = Selector('Mutation')({
|
||||
[name]: [{ data: $('data', 'AddBranchWhitelistInput!') }, branchSelector],
|
||||
})
|
||||
|
||||
export interface IInput {
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
[key: string]: unknown
|
||||
|
||||
data: ModelTypes['AddBranchWhitelistInput']
|
||||
}
|
||||
|
||||
export type IOutput = InputType<GraphQLTypes['Mutation'], typeof mutation>
|
||||
@@ -0,0 +1,19 @@
|
||||
import { type branchModel, branchSelector } from '../../selectors'
|
||||
import { $, type GraphQLTypes, type InputType, type ModelTypes, Selector } from '../../zeus/index'
|
||||
|
||||
export const name = 'deleteBranchWhitelist'
|
||||
|
||||
export const mutation = Selector('Mutation')({
|
||||
[name]: [{ data: $('data', 'DeleteBranchWhitelistInput!') }, branchSelector],
|
||||
})
|
||||
|
||||
export interface IInput {
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
[key: string]: unknown
|
||||
|
||||
data: ModelTypes['DeleteBranchWhitelistInput']
|
||||
}
|
||||
|
||||
export type IOutput = InputType<GraphQLTypes['Mutation'], typeof mutation>
|
||||
@@ -1,7 +1,13 @@
|
||||
/** Добавить доверенное лицо кооперативного участка */
|
||||
export * as AddTrustedAccount from './addTrustedAccount'
|
||||
/** Добавить пайщика в белый список приватного кооперативного участка */
|
||||
export * as AddBranchWhitelist from './addBranchWhitelist'
|
||||
/** Создать кооперативный участок */
|
||||
export * as CreateBranch from './createBranch'
|
||||
/** Удалить пайщика из белого списка приватного кооперативного участка */
|
||||
export * as DeleteBranchWhitelist from './deleteBranchWhitelist'
|
||||
/** Установить приватность кооперативного участка */
|
||||
export * as SetBranchPrivate from './setBranchPrivate'
|
||||
/** Удалить кооперативный участок */
|
||||
export * as DeleteBranch from './deleteBranch'
|
||||
/** Удалить доверенное лицо кооперативного участка */
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import { type branchModel, branchSelector } from '../../selectors'
|
||||
import { $, type GraphQLTypes, type InputType, type ModelTypes, Selector } from '../../zeus/index'
|
||||
|
||||
export const name = 'setBranchPrivate'
|
||||
|
||||
export const mutation = Selector('Mutation')({
|
||||
[name]: [{ data: $('data', 'SetBranchPrivateInput!') }, branchSelector],
|
||||
})
|
||||
|
||||
export interface IInput {
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
[key: string]: unknown
|
||||
|
||||
data: ModelTypes['SetBranchPrivateInput']
|
||||
}
|
||||
|
||||
export type IOutput = InputType<GraphQLTypes['Mutation'], typeof mutation>
|
||||
Reference in New Issue
Block a user