[C28-38][@ant] правка: управление приватностью КУ — на стол председателя
Перенёс управление приватностью+белым списком в MemberBranchListPage (стол председателя «Кооперативные участки», развёрнутая строка) через компонент BranchPrivacyManager. Откатил KuBranchesPage (стол «Кооперативный участок») к виду без управления — там оно было не к месту. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1 +1,2 @@
|
||||
export * from './model'
|
||||
export { default as BranchPrivacyManager } from './ui/BranchPrivacyManager.vue'
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
<template lang="pug">
|
||||
.branch-privacy
|
||||
BaseCheckbox(
|
||||
:model-value='isPrivate',
|
||||
:disabled='busy',
|
||||
label='Приватный участок — выбрать его при вступлении или смене может только пайщик из белого списка',
|
||||
@update:model-value='onTogglePrivate'
|
||||
)
|
||||
template(v-if='isPrivate')
|
||||
.branch-privacy__subtitle.t-sm.t-muted Белый список пайщиков
|
||||
.branch-privacy__add
|
||||
UserSearchSelector.branch-privacy__search(
|
||||
v-model='selected',
|
||||
label='Начните ввод ФИО пайщика',
|
||||
dense
|
||||
)
|
||||
BaseButton(
|
||||
variant='primary',
|
||||
:disabled='!selected',
|
||||
:loading='busy',
|
||||
@click='onAdd'
|
||||
) Добавить
|
||||
.branch-privacy__list(v-if='whitelist.length')
|
||||
.branch-privacy__member(v-for='member in whitelist', :key='member.username')
|
||||
span {{ memberName(member) }}
|
||||
button.icon-btn(
|
||||
type='button',
|
||||
aria-label='Удалить из белого списка',
|
||||
:disabled='busy',
|
||||
@click='onRemove(member.username)'
|
||||
)
|
||||
q-icon(name='close')
|
||||
.t-sm.t-muted(v-else) Белый список пуст — добавьте пайщиков, которым разрешён выбор этого участка.
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue';
|
||||
import { useBranchPrivacy } from '../model';
|
||||
import { useSystemStore } from 'src/entities/System/model';
|
||||
import type { IBranch } from 'src/entities/Branch/model';
|
||||
import { FailAlert, SuccessAlert } from 'src/shared/api';
|
||||
import { BaseButton, BaseCheckbox } from 'src/shared/ui/base';
|
||||
import { UserSearchSelector } from 'src/shared/ui';
|
||||
|
||||
const props = defineProps<{ branch: IBranch }>();
|
||||
|
||||
const { info } = useSystemStore();
|
||||
const { setBranchPrivate, addBranchWhitelist, deleteBranchWhitelist } = useBranchPrivacy();
|
||||
|
||||
const busy = ref(false);
|
||||
const selected = ref('');
|
||||
|
||||
const isPrivate = computed(() => props.branch.is_private);
|
||||
const whitelist = computed(() => props.branch.whitelist_certificates ?? []);
|
||||
|
||||
function memberName(member: any): string {
|
||||
return [member.last_name, member.first_name, member.middle_name].filter(Boolean).join(' ') || member.username;
|
||||
}
|
||||
|
||||
async function onTogglePrivate(value: boolean) {
|
||||
busy.value = true;
|
||||
try {
|
||||
await setBranchPrivate({ coopname: info.coopname, braname: props.branch.braname, is_private: value });
|
||||
SuccessAlert(value ? 'Участок сделан приватным' : 'Участок сделан публичным');
|
||||
} catch (e: unknown) {
|
||||
FailAlert(e);
|
||||
} finally {
|
||||
busy.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function onAdd() {
|
||||
if (!selected.value) return;
|
||||
busy.value = true;
|
||||
try {
|
||||
await addBranchWhitelist({ coopname: info.coopname, braname: props.branch.braname, account: selected.value });
|
||||
selected.value = '';
|
||||
SuccessAlert('Пайщик добавлен в белый список участка');
|
||||
} catch (e: unknown) {
|
||||
FailAlert(e);
|
||||
} finally {
|
||||
busy.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function onRemove(account: string) {
|
||||
busy.value = true;
|
||||
try {
|
||||
await deleteBranchWhitelist({ coopname: info.coopname, braname: props.branch.braname, account });
|
||||
SuccessAlert('Пайщик удалён из белого списка участка');
|
||||
} catch (e: unknown) {
|
||||
FailAlert(e);
|
||||
} finally {
|
||||
busy.value = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.branch-privacy {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--p-3);
|
||||
}
|
||||
|
||||
.branch-privacy__subtitle {
|
||||
margin-top: var(--p-2);
|
||||
}
|
||||
|
||||
.branch-privacy__add {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: var(--p-3);
|
||||
}
|
||||
|
||||
.branch-privacy__search {
|
||||
flex: 1 1 auto;
|
||||
max-width: 420px;
|
||||
}
|
||||
|
||||
.branch-privacy__list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--p-2);
|
||||
max-width: 420px;
|
||||
}
|
||||
|
||||
.branch-privacy__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>
|
||||
@@ -57,6 +57,10 @@
|
||||
p.t-muted для замены председателя участка — измените его имя аккаунта в карточке участка на аккаунт одного из пайщиков.
|
||||
div.q-mt-md.flex.justify-center
|
||||
DeleteBranchButton(:branch="props.row")
|
||||
div.row
|
||||
div.col-12.q-pa-sm
|
||||
p.text-center.text-overline доступ к участку
|
||||
BranchPrivacyManager(:branch="props.row")
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
@@ -65,6 +69,7 @@ import { useBranchStore } from 'src/entities/Branch/model';
|
||||
import { useEditableTableRows } from 'src/shared/lib/composables/useEditableTableRows';
|
||||
import { CreateBranchButton } from 'src/features/Branch/CreateBranch';
|
||||
import { DeleteBranchButton } from 'src/features/Branch/DeleteBranch';
|
||||
import { BranchPrivacyManager } from 'src/features/Branch/BranchPrivacy';
|
||||
import { getNameFromUserData } from 'src/shared/lib/utils/getNameFromUserData';
|
||||
import { BranchCard } from 'src/widgets/BranchCard';
|
||||
import { EditableIndividualCard } from 'src/shared/ui/EditableIndividualCard';
|
||||
|
||||
@@ -25,80 +25,33 @@
|
||||
table.table
|
||||
thead
|
||||
tr
|
||||
th.col-expand(v-if='isChairman')
|
||||
th Участок
|
||||
th Председатель
|
||||
th Доверенные
|
||||
th.col-count Пайщики
|
||||
th.col-action
|
||||
tbody
|
||||
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) Белый список пуст — добавьте пайщиков, которым разрешён выбор этого участка.
|
||||
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')
|
||||
EmptyState(
|
||||
v-else,
|
||||
title='Кооперативных участков пока нет',
|
||||
@@ -109,19 +62,14 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, reactive, ref } from 'vue';
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useBranchStore, type IBranch, type IPublicBranch } from 'src/entities/Branch/model';
|
||||
import { useBranchStore } 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, SuccessAlert } from 'src/shared/api';
|
||||
import { BaseButton, BaseCard, BaseCheckbox, BaseChip, EmptyState, TableSkeleton } from 'src/shared/ui/base';
|
||||
import { FailAlert } from 'src/shared/api';
|
||||
import { BaseCard, 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: 'Участок' },
|
||||
@@ -134,18 +82,12 @@ 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 isChairman = computed(() => session.isChairman);
|
||||
const branches = computed<AnyBranch[]>(() =>
|
||||
isChairman.value ? branchStore.branches : branchStore.publicBranches,
|
||||
);
|
||||
// публичные данные участков (сертификаты с ФИО) — доступны любому пайщику
|
||||
const branches = computed(() => branchStore.publicBranches);
|
||||
|
||||
// состояние системы управления из контракта: is_branched отдаётся системным стором
|
||||
// (system.info.cooperator_account). Контракт включает флаг при 3+ кооперативных участках.
|
||||
@@ -156,27 +98,7 @@ const branchModeText = computed(() =>
|
||||
: 'Пока в кооперативе меньше трёх кооперативных участков — все пайщики участвуют в общем собрании напрямую. Когда участков станет три и более, система управления автоматически переключится на мажоритарную: собрания будут проходить через кооперативные участки.',
|
||||
);
|
||||
|
||||
// локальное состояние разворота строк и формы белого списка (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 {
|
||||
function branchTitle(branch: any): string {
|
||||
return branch.short_name || branch.full_name || branch.braname;
|
||||
}
|
||||
|
||||
@@ -188,11 +110,11 @@ function certificateName(certificate: any): string {
|
||||
);
|
||||
}
|
||||
|
||||
function chairmanName(branch: AnyBranch): string {
|
||||
function chairmanName(branch: any): string {
|
||||
return certificateName(branch.trustee_certificate);
|
||||
}
|
||||
|
||||
function trustedNames(branch: AnyBranch): string[] {
|
||||
function trustedNames(branch: any): string[] {
|
||||
return (branch.trusted_certificates ?? []).map((certificate: any) => certificateName(certificate));
|
||||
}
|
||||
|
||||
@@ -200,53 +122,10 @@ 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 {
|
||||
if (isChairman.value) {
|
||||
await branchStore.loadBranches({ coopname: system.info.coopname });
|
||||
} else {
|
||||
await branchStore.loadPublicBranches({ coopname: system.info.coopname });
|
||||
}
|
||||
await branchStore.loadPublicBranches({ coopname: system.info.coopname });
|
||||
} catch (e: unknown) {
|
||||
FailAlert(e);
|
||||
} finally {
|
||||
@@ -266,55 +145,4 @@ 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>
|
||||
|
||||
Reference in New Issue
Block a user