2026-05-23 12:21:21 +03:00
|
|
|
|
<script setup lang="ts">
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Adminка SaaS → Обращения субъектов ПДн (152-ФЗ).
|
|
|
|
|
|
*
|
|
|
|
|
|
* Список обращений на удаление/доступ/исправление/возражение.
|
|
|
|
|
|
* Для request_type='deletion' — кнопка «Анонимизировать» (§ 1.5, дыра #4).
|
|
|
|
|
|
*
|
|
|
|
|
|
* API: GET/POST /api/admin/pd-subject-requests, POST /{id}/erase
|
|
|
|
|
|
*/
|
|
|
|
|
|
import { onMounted, ref, reactive, computed } from 'vue';
|
|
|
|
|
|
import * as adminApi from '../../api/admin';
|
|
|
|
|
|
import type { PdSubjectRequest, CreatePdRequestPayload } from '../../api/admin';
|
|
|
|
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
// State
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
const rows = ref<PdSubjectRequest[]>([]);
|
|
|
|
|
|
const total = ref(0);
|
|
|
|
|
|
const loading = ref(false);
|
|
|
|
|
|
const fetchError = ref(false);
|
|
|
|
|
|
|
|
|
|
|
|
const filterStatus = ref('');
|
|
|
|
|
|
const filterType = ref('');
|
|
|
|
|
|
|
|
|
|
|
|
// Dialog: create
|
|
|
|
|
|
const createDialog = ref(false);
|
|
|
|
|
|
const createLoading = ref(false);
|
|
|
|
|
|
const createError = ref('');
|
|
|
|
|
|
const createForm = reactive<CreatePdRequestPayload>({
|
|
|
|
|
|
subject_email: '',
|
|
|
|
|
|
subject_phone: '',
|
|
|
|
|
|
subject_full_name: '',
|
|
|
|
|
|
request_type: 'deletion',
|
|
|
|
|
|
description: '',
|
|
|
|
|
|
tenant_id: null,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// Dialog: erase confirm
|
|
|
|
|
|
const eraseDialog = ref(false);
|
|
|
|
|
|
const eraseLoading = ref(false);
|
2026-06-21 17:08:51 +03:00
|
|
|
|
// UI-аудит: window.alert → v-snackbar.
|
|
|
|
|
|
const errToastOpen = ref(false);
|
|
|
|
|
|
const errToastText = ref('');
|
2026-05-23 12:21:21 +03:00
|
|
|
|
const eraseTarget = ref<PdSubjectRequest | null>(null);
|
|
|
|
|
|
const eraseResult = ref<{ users: number; leads: number; deals: number; webhook_log: number } | null>(null);
|
|
|
|
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
// Load data
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
async function loadRows(): Promise<void> {
|
|
|
|
|
|
loading.value = true;
|
|
|
|
|
|
fetchError.value = false;
|
|
|
|
|
|
try {
|
|
|
|
|
|
const res = await adminApi.listPdSubjectRequests({
|
|
|
|
|
|
status: filterStatus.value || undefined,
|
|
|
|
|
|
request_type: filterType.value || undefined,
|
|
|
|
|
|
limit: 100,
|
|
|
|
|
|
offset: 0,
|
|
|
|
|
|
});
|
|
|
|
|
|
rows.value = res.data;
|
|
|
|
|
|
total.value = res.total;
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
fetchError.value = true;
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
loading.value = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
onMounted(loadRows);
|
|
|
|
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
// Create request
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
async function submitCreate(): Promise<void> {
|
|
|
|
|
|
createError.value = '';
|
|
|
|
|
|
if (!createForm.subject_email && !createForm.subject_phone) {
|
|
|
|
|
|
createError.value = 'Укажите email или телефон субъекта.';
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
createLoading.value = true;
|
|
|
|
|
|
try {
|
|
|
|
|
|
await adminApi.createPdSubjectRequest({
|
|
|
|
|
|
subject_email: createForm.subject_email || undefined,
|
|
|
|
|
|
subject_phone: createForm.subject_phone || undefined,
|
|
|
|
|
|
subject_full_name: createForm.subject_full_name || undefined,
|
|
|
|
|
|
request_type: createForm.request_type,
|
|
|
|
|
|
description: createForm.description || undefined,
|
|
|
|
|
|
tenant_id: createForm.tenant_id ?? undefined,
|
|
|
|
|
|
});
|
|
|
|
|
|
createDialog.value = false;
|
|
|
|
|
|
resetCreateForm();
|
|
|
|
|
|
await loadRows();
|
|
|
|
|
|
} catch (e: unknown) {
|
|
|
|
|
|
const err = e as { response?: { data?: { message?: string } } };
|
|
|
|
|
|
createError.value = err?.response?.data?.message ?? 'Ошибка при создании обращения.';
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
createLoading.value = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function resetCreateForm(): void {
|
|
|
|
|
|
createForm.subject_email = '';
|
|
|
|
|
|
createForm.subject_phone = '';
|
|
|
|
|
|
createForm.subject_full_name = '';
|
|
|
|
|
|
createForm.request_type = 'deletion';
|
|
|
|
|
|
createForm.description = '';
|
|
|
|
|
|
createForm.tenant_id = null;
|
|
|
|
|
|
createError.value = '';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
// Erase
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
function openErase(row: PdSubjectRequest): void {
|
|
|
|
|
|
eraseTarget.value = row;
|
|
|
|
|
|
eraseResult.value = null;
|
|
|
|
|
|
eraseDialog.value = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function confirmErase(): Promise<void> {
|
|
|
|
|
|
if (!eraseTarget.value) return;
|
|
|
|
|
|
eraseLoading.value = true;
|
|
|
|
|
|
try {
|
|
|
|
|
|
const res = await adminApi.executePdErasure(eraseTarget.value.id);
|
|
|
|
|
|
eraseResult.value = res.counts;
|
|
|
|
|
|
// Update row status in list
|
|
|
|
|
|
const idx = rows.value.findIndex((r) => r.id === eraseTarget.value?.id);
|
|
|
|
|
|
if (idx !== -1) {
|
|
|
|
|
|
rows.value[idx] = { ...rows.value[idx], status: 'completed' };
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (e: unknown) {
|
|
|
|
|
|
const err = e as { response?: { data?: { message?: string } } };
|
2026-06-21 17:08:51 +03:00
|
|
|
|
errToastText.value = err?.response?.data?.message ?? 'Ошибка анонимизации.';
|
|
|
|
|
|
errToastOpen.value = true;
|
2026-05-23 12:21:21 +03:00
|
|
|
|
eraseDialog.value = false;
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
eraseLoading.value = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
// Helpers
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
const statusLabels: Record<string, { label: string; color: string }> = {
|
2026-06-17 05:17:12 +03:00
|
|
|
|
received: { label: 'Получено', color: 'info' },
|
|
|
|
|
|
in_progress: { label: 'В работе', color: 'warning' },
|
|
|
|
|
|
completed: { label: 'Выполнено', color: 'success' },
|
|
|
|
|
|
rejected: { label: 'Отклонено', color: 'error' },
|
2026-05-23 12:21:21 +03:00
|
|
|
|
};
|
|
|
|
|
|
function statusInfo(s: string) {
|
|
|
|
|
|
return statusLabels[s] ?? { label: s, color: 'default' };
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const typeLabels: Record<string, string> = {
|
2026-06-17 05:17:12 +03:00
|
|
|
|
access: 'Доступ',
|
|
|
|
|
|
rectification: 'Исправление',
|
|
|
|
|
|
deletion: 'Удаление',
|
|
|
|
|
|
objection: 'Возражение',
|
2026-05-23 12:21:21 +03:00
|
|
|
|
};
|
|
|
|
|
|
function typeLabel(t: string): string {
|
|
|
|
|
|
return typeLabels[t] ?? t;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function formatDate(iso: string | null): string {
|
|
|
|
|
|
if (!iso) return '—';
|
|
|
|
|
|
return new Date(iso).toLocaleString('ru-RU', {
|
2026-06-17 05:17:12 +03:00
|
|
|
|
day: '2-digit',
|
|
|
|
|
|
month: '2-digit',
|
|
|
|
|
|
year: 'numeric',
|
|
|
|
|
|
hour: '2-digit',
|
|
|
|
|
|
minute: '2-digit',
|
2026-05-23 12:21:21 +03:00
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const headers = [
|
2026-06-17 05:17:12 +03:00
|
|
|
|
{ title: 'ID', key: 'id', width: '60px' },
|
|
|
|
|
|
{ title: 'Получено', key: 'received_at', width: '140px' },
|
|
|
|
|
|
{ title: 'Email / тел.', key: 'contact', sortable: false },
|
|
|
|
|
|
{ title: 'Тип', key: 'request_type', width: '110px' },
|
|
|
|
|
|
{ title: 'Статус', key: 'status', width: '120px' },
|
|
|
|
|
|
{ title: 'Дедлайн', key: 'deadline_at', width: '140px' },
|
|
|
|
|
|
{ title: 'Действия', key: 'actions', sortable: false, width: '140px', align: 'end' as const },
|
2026-05-23 12:21:21 +03:00
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
const filteredRows = computed(() => rows.value);
|
|
|
|
|
|
|
|
|
|
|
|
defineExpose({ rows, loading, fetchError, loadRows });
|
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<template>
|
|
|
|
|
|
<v-container fluid class="admin-pd pa-6">
|
|
|
|
|
|
<!-- Page head -->
|
|
|
|
|
|
<header class="page-head mb-4 d-flex justify-space-between align-start flex-wrap ga-3">
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<h1 class="text-h4 page-title">Обращения субъектов ПДн</h1>
|
|
|
|
|
|
<p class="text-body-2 text-medium-emphasis ma-0">
|
2026-06-17 05:17:12 +03:00
|
|
|
|
Обращения на доступ, исправление, удаление и возражение (152-ФЗ). Срок ответа — 30 дней.
|
2026-05-23 12:21:21 +03:00
|
|
|
|
</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="d-flex ga-2">
|
|
|
|
|
|
<v-btn
|
|
|
|
|
|
variant="outlined"
|
|
|
|
|
|
prepend-icon="mdi-refresh"
|
|
|
|
|
|
:loading="loading"
|
|
|
|
|
|
data-testid="reload-btn"
|
|
|
|
|
|
@click="loadRows"
|
|
|
|
|
|
>
|
|
|
|
|
|
Обновить
|
|
|
|
|
|
</v-btn>
|
2026-06-17 05:17:12 +03:00
|
|
|
|
<v-btn color="primary" prepend-icon="mdi-plus" data-testid="create-btn" @click="createDialog = true">
|
2026-05-23 12:21:21 +03:00
|
|
|
|
Новый запрос
|
|
|
|
|
|
</v-btn>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</header>
|
|
|
|
|
|
|
|
|
|
|
|
<v-alert
|
|
|
|
|
|
v-if="fetchError"
|
|
|
|
|
|
type="warning"
|
|
|
|
|
|
variant="tonal"
|
|
|
|
|
|
density="compact"
|
|
|
|
|
|
closable
|
|
|
|
|
|
class="mb-4"
|
|
|
|
|
|
data-testid="fetch-error-alert"
|
|
|
|
|
|
>
|
|
|
|
|
|
Не удалось загрузить обращения. Попробуйте обновить.
|
|
|
|
|
|
</v-alert>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Filters -->
|
|
|
|
|
|
<v-card variant="outlined" class="pa-3 mb-4">
|
|
|
|
|
|
<v-row dense>
|
|
|
|
|
|
<v-col cols="12" sm="4">
|
|
|
|
|
|
<v-select
|
|
|
|
|
|
v-model="filterStatus"
|
|
|
|
|
|
label="Статус"
|
|
|
|
|
|
:items="[
|
|
|
|
|
|
{ title: 'Все статусы', value: '' },
|
2026-06-17 05:17:12 +03:00
|
|
|
|
{ title: 'Получено', value: 'received' },
|
|
|
|
|
|
{ title: 'В работе', value: 'in_progress' },
|
|
|
|
|
|
{ title: 'Выполнено', value: 'completed' },
|
|
|
|
|
|
{ title: 'Отклонено', value: 'rejected' },
|
2026-05-23 12:21:21 +03:00
|
|
|
|
]"
|
|
|
|
|
|
density="compact"
|
|
|
|
|
|
variant="outlined"
|
|
|
|
|
|
hide-details
|
|
|
|
|
|
@update:model-value="loadRows"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</v-col>
|
|
|
|
|
|
<v-col cols="12" sm="4">
|
|
|
|
|
|
<v-select
|
|
|
|
|
|
v-model="filterType"
|
|
|
|
|
|
label="Тип обращения"
|
|
|
|
|
|
:items="[
|
2026-06-17 05:17:12 +03:00
|
|
|
|
{ title: 'Все типы', value: '' },
|
|
|
|
|
|
{ title: 'Доступ', value: 'access' },
|
2026-05-23 12:21:21 +03:00
|
|
|
|
{ title: 'Исправление', value: 'rectification' },
|
2026-06-17 05:17:12 +03:00
|
|
|
|
{ title: 'Удаление', value: 'deletion' },
|
|
|
|
|
|
{ title: 'Возражение', value: 'objection' },
|
2026-05-23 12:21:21 +03:00
|
|
|
|
]"
|
|
|
|
|
|
density="compact"
|
|
|
|
|
|
variant="outlined"
|
|
|
|
|
|
hide-details
|
|
|
|
|
|
@update:model-value="loadRows"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</v-col>
|
|
|
|
|
|
<v-col cols="12" sm="4" class="d-flex align-center">
|
|
|
|
|
|
<span class="text-body-2 text-medium-emphasis">Всего: {{ total }}</span>
|
|
|
|
|
|
</v-col>
|
|
|
|
|
|
</v-row>
|
|
|
|
|
|
</v-card>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Table -->
|
|
|
|
|
|
<v-card variant="outlined">
|
|
|
|
|
|
<v-data-table
|
|
|
|
|
|
:headers="headers"
|
|
|
|
|
|
:items="filteredRows"
|
|
|
|
|
|
:loading="loading"
|
|
|
|
|
|
item-value="id"
|
|
|
|
|
|
density="compact"
|
|
|
|
|
|
no-data-text="Обращений нет"
|
|
|
|
|
|
data-testid="pd-requests-table"
|
|
|
|
|
|
>
|
2026-06-17 07:22:08 +03:00
|
|
|
|
<template #[`item.received_at`]="{ item }">
|
2026-05-23 12:21:21 +03:00
|
|
|
|
<span class="text-caption font-mono">{{ formatDate(item.received_at) }}</span>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
2026-06-17 07:22:08 +03:00
|
|
|
|
<template #[`item.contact`]="{ item }">
|
2026-05-23 12:21:21 +03:00
|
|
|
|
<div>
|
|
|
|
|
|
<span v-if="item.subject_email" class="d-block text-body-2">{{ item.subject_email }}</span>
|
|
|
|
|
|
<span v-if="item.subject_phone" class="d-block text-caption text-medium-emphasis">
|
|
|
|
|
|
{{ item.subject_phone }}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
<span v-if="item.subject_full_name" class="d-block text-caption text-medium-emphasis">
|
|
|
|
|
|
{{ item.subject_full_name }}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
2026-06-17 07:22:08 +03:00
|
|
|
|
<template #[`item.request_type`]="{ item }">
|
2026-05-23 12:21:21 +03:00
|
|
|
|
<v-chip
|
|
|
|
|
|
:color="item.request_type === 'deletion' ? 'error' : 'default'"
|
|
|
|
|
|
size="x-small"
|
|
|
|
|
|
variant="tonal"
|
|
|
|
|
|
>
|
|
|
|
|
|
{{ typeLabel(item.request_type) }}
|
|
|
|
|
|
</v-chip>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
2026-06-17 07:22:08 +03:00
|
|
|
|
<template #[`item.status`]="{ item }">
|
2026-06-17 05:17:12 +03:00
|
|
|
|
<v-chip :color="statusInfo(item.status).color" size="x-small" variant="tonal">
|
2026-05-23 12:21:21 +03:00
|
|
|
|
{{ statusInfo(item.status).label }}
|
|
|
|
|
|
</v-chip>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
2026-06-17 07:22:08 +03:00
|
|
|
|
<template #[`item.deadline_at`]="{ item }">
|
2026-05-23 12:21:21 +03:00
|
|
|
|
<span
|
|
|
|
|
|
class="text-caption"
|
2026-06-17 05:17:12 +03:00
|
|
|
|
:class="
|
|
|
|
|
|
item.status !== 'completed' && new Date(item.deadline_at) < new Date() ? 'text-error' : ''
|
|
|
|
|
|
"
|
2026-05-23 12:21:21 +03:00
|
|
|
|
>
|
|
|
|
|
|
{{ formatDate(item.deadline_at) }}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
2026-06-17 07:22:08 +03:00
|
|
|
|
<template #[`item.actions`]="{ item }">
|
2026-05-23 12:21:21 +03:00
|
|
|
|
<v-btn
|
|
|
|
|
|
v-if="item.request_type === 'deletion' && item.status !== 'completed'"
|
|
|
|
|
|
color="error"
|
|
|
|
|
|
size="x-small"
|
|
|
|
|
|
variant="tonal"
|
|
|
|
|
|
prepend-icon="mdi-delete-forever"
|
|
|
|
|
|
:data-testid="`erase-btn-${item.id}`"
|
|
|
|
|
|
@click="openErase(item)"
|
|
|
|
|
|
>
|
|
|
|
|
|
Анонимизировать
|
|
|
|
|
|
</v-btn>
|
2026-06-17 05:17:12 +03:00
|
|
|
|
<v-chip v-else-if="item.status === 'completed'" color="success" size="x-small" variant="text">
|
2026-05-23 12:21:21 +03:00
|
|
|
|
Выполнено
|
|
|
|
|
|
</v-chip>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</v-data-table>
|
|
|
|
|
|
</v-card>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Dialog: create request -->
|
|
|
|
|
|
<v-dialog v-model="createDialog" max-width="520" data-testid="create-dialog">
|
|
|
|
|
|
<v-card>
|
|
|
|
|
|
<v-card-title class="text-h6 pa-4 pb-2">Новое обращение субъекта ПДн</v-card-title>
|
|
|
|
|
|
<v-card-text class="pa-4 pt-0">
|
2026-06-17 05:17:12 +03:00
|
|
|
|
<v-alert v-if="createError" type="error" variant="tonal" density="compact" class="mb-3">
|
2026-05-23 12:21:21 +03:00
|
|
|
|
{{ createError }}
|
|
|
|
|
|
</v-alert>
|
|
|
|
|
|
|
|
|
|
|
|
<v-select
|
|
|
|
|
|
v-model="createForm.request_type"
|
|
|
|
|
|
label="Тип обращения *"
|
|
|
|
|
|
:items="[
|
2026-06-17 05:17:12 +03:00
|
|
|
|
{ title: 'Доступ к данным', value: 'access' },
|
2026-05-23 12:21:21 +03:00
|
|
|
|
{ title: 'Исправление данных', value: 'rectification' },
|
2026-06-17 05:17:12 +03:00
|
|
|
|
{ title: 'Удаление данных', value: 'deletion' },
|
|
|
|
|
|
{ title: 'Возражение', value: 'objection' },
|
2026-05-23 12:21:21 +03:00
|
|
|
|
]"
|
|
|
|
|
|
density="compact"
|
|
|
|
|
|
variant="outlined"
|
|
|
|
|
|
class="mb-3"
|
|
|
|
|
|
data-testid="form-request-type"
|
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
|
|
<v-text-field
|
|
|
|
|
|
v-model="createForm.subject_email"
|
|
|
|
|
|
label="Email субъекта"
|
|
|
|
|
|
type="email"
|
|
|
|
|
|
density="compact"
|
|
|
|
|
|
variant="outlined"
|
|
|
|
|
|
class="mb-2"
|
|
|
|
|
|
data-testid="form-email"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<v-text-field
|
|
|
|
|
|
v-model="createForm.subject_phone"
|
|
|
|
|
|
label="Телефон субъекта"
|
|
|
|
|
|
density="compact"
|
|
|
|
|
|
variant="outlined"
|
|
|
|
|
|
class="mb-2"
|
|
|
|
|
|
data-testid="form-phone"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<v-text-field
|
|
|
|
|
|
v-model="createForm.subject_full_name"
|
|
|
|
|
|
label="ФИО субъекта"
|
|
|
|
|
|
density="compact"
|
|
|
|
|
|
variant="outlined"
|
|
|
|
|
|
class="mb-2"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<v-text-field
|
|
|
|
|
|
v-model.number="createForm.tenant_id"
|
|
|
|
|
|
label="ID тенанта (необязательно)"
|
|
|
|
|
|
type="number"
|
|
|
|
|
|
density="compact"
|
|
|
|
|
|
variant="outlined"
|
|
|
|
|
|
class="mb-2"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<v-textarea
|
|
|
|
|
|
v-model="createForm.description"
|
|
|
|
|
|
label="Описание"
|
|
|
|
|
|
density="compact"
|
|
|
|
|
|
variant="outlined"
|
|
|
|
|
|
rows="3"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</v-card-text>
|
|
|
|
|
|
<v-card-actions class="pa-4 pt-0 justify-end">
|
2026-06-17 05:17:12 +03:00
|
|
|
|
<v-btn
|
|
|
|
|
|
variant="text"
|
|
|
|
|
|
@click="
|
|
|
|
|
|
createDialog = false;
|
|
|
|
|
|
resetCreateForm();
|
|
|
|
|
|
"
|
|
|
|
|
|
>Отмена</v-btn
|
|
|
|
|
|
>
|
2026-05-23 12:21:21 +03:00
|
|
|
|
<v-btn
|
|
|
|
|
|
color="primary"
|
|
|
|
|
|
:loading="createLoading"
|
|
|
|
|
|
data-testid="submit-create-btn"
|
|
|
|
|
|
@click="submitCreate"
|
|
|
|
|
|
>
|
|
|
|
|
|
Создать
|
|
|
|
|
|
</v-btn>
|
|
|
|
|
|
</v-card-actions>
|
|
|
|
|
|
</v-card>
|
|
|
|
|
|
</v-dialog>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Dialog: erase confirm -->
|
|
|
|
|
|
<v-dialog v-model="eraseDialog" max-width="480" data-testid="erase-dialog">
|
|
|
|
|
|
<v-card>
|
2026-06-17 05:17:12 +03:00
|
|
|
|
<v-card-title class="text-h6 pa-4 pb-2 text-error"> Анонимизировать данные субъекта </v-card-title>
|
2026-05-23 12:21:21 +03:00
|
|
|
|
<v-card-text class="pa-4 pt-0">
|
|
|
|
|
|
<template v-if="!eraseResult">
|
|
|
|
|
|
<v-alert type="warning" variant="tonal" density="compact" class="mb-3">
|
|
|
|
|
|
Операция необратима. Данные будут заменены плейсхолдерами.
|
|
|
|
|
|
</v-alert>
|
2026-06-17 05:17:12 +03:00
|
|
|
|
<p class="text-body-2 mb-1"><strong>Email:</strong> {{ eraseTarget?.subject_email ?? '—' }}</p>
|
2026-05-23 12:21:21 +03:00
|
|
|
|
<p class="text-body-2 mb-1">
|
|
|
|
|
|
<strong>Телефон:</strong> {{ eraseTarget?.subject_phone ?? '—' }}
|
|
|
|
|
|
</p>
|
2026-06-17 05:17:12 +03:00
|
|
|
|
<p class="text-body-2"><strong>Тенант:</strong> {{ eraseTarget?.tenant_id ?? 'все' }}</p>
|
2026-05-23 12:21:21 +03:00
|
|
|
|
</template>
|
|
|
|
|
|
<template v-else>
|
|
|
|
|
|
<v-alert type="success" variant="tonal" density="compact" class="mb-3">
|
|
|
|
|
|
Анонимизация выполнена.
|
|
|
|
|
|
</v-alert>
|
2026-06-17 05:17:12 +03:00
|
|
|
|
<p class="text-body-2 mb-1">
|
|
|
|
|
|
Пользователей: <strong>{{ eraseResult.users }}</strong>
|
|
|
|
|
|
</p>
|
|
|
|
|
|
<p class="text-body-2 mb-1">
|
|
|
|
|
|
Лидов поставщика: <strong>{{ eraseResult.leads }}</strong>
|
|
|
|
|
|
</p>
|
|
|
|
|
|
<p class="text-body-2 mb-1">
|
|
|
|
|
|
Сделок: <strong>{{ eraseResult.deals }}</strong>
|
|
|
|
|
|
</p>
|
|
|
|
|
|
<p class="text-body-2">
|
|
|
|
|
|
Webhook-логов: <strong>{{ eraseResult.webhook_log }}</strong>
|
|
|
|
|
|
</p>
|
2026-05-23 12:21:21 +03:00
|
|
|
|
</template>
|
|
|
|
|
|
</v-card-text>
|
|
|
|
|
|
<v-card-actions class="pa-4 pt-0 justify-end">
|
2026-06-17 05:17:12 +03:00
|
|
|
|
<v-btn variant="text" @click="eraseDialog = false">
|
2026-05-23 12:21:21 +03:00
|
|
|
|
{{ eraseResult ? 'Закрыть' : 'Отмена' }}
|
|
|
|
|
|
</v-btn>
|
|
|
|
|
|
<v-btn
|
|
|
|
|
|
v-if="!eraseResult"
|
|
|
|
|
|
color="error"
|
|
|
|
|
|
:loading="eraseLoading"
|
|
|
|
|
|
data-testid="confirm-erase-btn"
|
|
|
|
|
|
@click="confirmErase"
|
|
|
|
|
|
>
|
|
|
|
|
|
Подтвердить удаление
|
|
|
|
|
|
</v-btn>
|
|
|
|
|
|
</v-card-actions>
|
|
|
|
|
|
</v-card>
|
|
|
|
|
|
</v-dialog>
|
2026-06-21 17:08:51 +03:00
|
|
|
|
|
|
|
|
|
|
<v-snackbar v-model="errToastOpen" :timeout="4000" color="error" location="bottom right">
|
|
|
|
|
|
{{ errToastText }}
|
|
|
|
|
|
</v-snackbar>
|
2026-05-23 12:21:21 +03:00
|
|
|
|
</v-container>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<style scoped>
|
|
|
|
|
|
.admin-pd {
|
|
|
|
|
|
max-width: 1200px;
|
|
|
|
|
|
}
|
|
|
|
|
|
.page-title {
|
|
|
|
|
|
font-variation-settings: 'opsz' 28;
|
|
|
|
|
|
letter-spacing: -0.018em;
|
|
|
|
|
|
}
|
|
|
|
|
|
.font-mono {
|
|
|
|
|
|
font-family: 'JetBrains Mono', ui-monospace, monospace;
|
|
|
|
|
|
}
|
|
|
|
|
|
</style>
|