Files
portal/app/resources/js/components/import/UnknownStatusesDialog.vue
T
Дмитрий 9ff19f3cc3 @
feat(bot): всеобъемлющая инструкция (36 статей) + 31 узкая экскурсия, которая открывает окна

Живой урок владельца 12.07.2026: «дубли чистятся в 2-х местах, а бот выдумал третье»,
«стыдно такого бота показывать», «видео показываем полное, а я спрашиваю про одно —
надо резать», «надо не только кнопку показывать, а и что за ней».

База знаний: 17 обзорных статей → 36 узких по темам. Портал обойдён целиком
(5 агентов): все экраны, окна, подсказки «(?)», тексты ошибок, внутренние правила.
Новое, чего бот не знал: дубли в ДВУХ местах (Поле и Предложения) + окно «источник
уже есть»; правило 18:00; почему приходит меньше лимита; оплата по счёту и акты;
списания; безопасность и 2FA; отчёты; массовые действия; регистрация.

Экскурсии: 15 → 31, каждая на свою тему (3–7 шагов). Раннер GuidedTour научен
шагу `open` — сам открывает окно/вкладку и подсвечивает то, что ВНУТРИ
(окно дублей, форму подбора, диалог пополнения, панель тарифов, карточку сделки).
99 новых якорей data-tour расставлено по всему порталу (4 агента).

Поиск: веса в tsvector (заголовок A / синонимы B / текст C) — иначе на «чистка
дублей» первой всплывала статья «Списания» из-за мимоходного упоминания «поля».

Тесты: Pest бот 39/39, Vitest 1222/1222. Новые сторожа: у каждой статьи есть
экскурсия; экскурсия не короче 3 и не длиннее 7 шагов; каждая цель экскурсии
реально существует в разметке; экскурсии про действия обязаны открывать окно.
Живая проверка в браузере: экскурсия про дубли сама открыла окно объединения
и перешла на второе место чистки.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@
2026-07-12 12:56:22 +03:00

113 lines
4.4 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
/**
* Wizard маппинга неизвестных статусов воронки из CSV-импорта (ТЗ §6.4/§6.6).
*
* Для каждого незамапленного русского статуса пользователь выбирает один из
* 5 slug'ов воронки. Сохранение → POST /api/imports/unknown-statuses/resolve.
*/
import { computed, reactive, ref } from 'vue';
import { resolveUnknownStatuses, type StatusMapping, type UnknownStatus } from '../../api/imports';
import { useLayoutTier } from '../../composables/useLayoutTier';
// Телефон: на весь экран (таблица сопоставления статусов широкая).
const { isPhone } = useLayoutTier();
const props = defineProps<{
modelValue: boolean;
statuses: UnknownStatus[];
}>();
const emit = defineEmits<{
'update:modelValue': [value: boolean];
resolved: [];
}>();
/** 5 статусов воронки (редизайн 2026-05-17). */
const STATUS_OPTIONS: { value: string; title: string }[] = [
{ value: 'new', title: 'Новая сделка' },
{ value: 'viewed', title: 'Просмотрено' },
{ value: 'in_progress', title: 'В работе' },
{ value: 'won', title: 'Сделка' },
{ value: 'lost', title: 'Не реализовано' },
];
const selection = reactive<Record<string, string | null>>({});
const saving = ref(false);
const error = ref<string | null>(null);
const dialogOpen = computed({
get: () => props.modelValue,
set: (v: boolean) => emit('update:modelValue', v),
});
const allMapped = computed(() => props.statuses.length > 0 && props.statuses.every((s) => !!selection[s.status_ru]));
async function save(): Promise<void> {
if (!allMapped.value) {
return;
}
saving.value = true;
error.value = null;
try {
const mappings: StatusMapping[] = props.statuses.map((s) => ({
status_ru: s.status_ru,
slug: selection[s.status_ru] as string,
}));
await resolveUnknownStatuses(mappings);
emit('resolved');
} catch {
error.value = 'Не удалось сохранить маппинг. Повторите попытку.';
} finally {
saving.value = false;
}
}
defineExpose({ selection, save });
</script>
<template>
<v-dialog v-model="dialogOpen" max-width="640" :fullscreen="isPhone" scrollable>
<v-card data-tour="import-map-dialog">
<v-card-title class="text-h6">Маппинг неизвестных статусов</v-card-title>
<v-card-text>
<p class="text-body-2 text-medium-emphasis mb-4">
Эти статусы из CSV не входят в стандартную воронку. Выберите соответствие повторный импорт
применит маппинг автоматически.
</p>
<div v-for="status in statuses" :key="status.id" class="d-flex align-center ga-3 mb-3">
<div class="flex-grow-1">
<strong>{{ status.status_ru }}</strong>
<span class="text-caption text-medium-emphasis ml-2"> ({{ status.occurrences }} шт.) </span>
</div>
<v-select
v-model="selection[status.status_ru]"
:items="STATUS_OPTIONS"
label="Статус воронки"
density="compact"
variant="outlined"
hide-details
style="max-width: 280px"
/>
</div>
<v-alert v-if="error" type="error" variant="tonal" class="mt-2">
{{ error }}
</v-alert>
</v-card-text>
<v-card-actions>
<v-spacer />
<v-btn variant="text" @click="dialogOpen = false">Отмена</v-btn>
<v-btn
data-test="save-mappings"
color="primary"
variant="flat"
:loading="saving"
:disabled="!allMapped"
@click="save"
>
Сохранить
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</template>