2026-05-16 19:58:33 +03:00
|
|
|
|
<script setup lang="ts">
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Wizard маппинга неизвестных статусов воронки из CSV-импорта (ТЗ §6.4/§6.6).
|
|
|
|
|
|
*
|
|
|
|
|
|
* Для каждого незамапленного русского статуса пользователь выбирает один из
|
2026-05-17 18:18:00 +03:00
|
|
|
|
* 5 slug'ов воронки. Сохранение → POST /api/imports/unknown-statuses/resolve.
|
2026-05-16 19:58:33 +03:00
|
|
|
|
*/
|
|
|
|
|
|
import { computed, reactive, ref } from 'vue';
|
|
|
|
|
|
import { resolveUnknownStatuses, type StatusMapping, type UnknownStatus } from '../../api/imports';
|
|
|
|
|
|
|
|
|
|
|
|
const props = defineProps<{
|
|
|
|
|
|
modelValue: boolean;
|
|
|
|
|
|
statuses: UnknownStatus[];
|
|
|
|
|
|
}>();
|
|
|
|
|
|
|
|
|
|
|
|
const emit = defineEmits<{
|
|
|
|
|
|
'update:modelValue': [value: boolean];
|
|
|
|
|
|
resolved: [];
|
|
|
|
|
|
}>();
|
|
|
|
|
|
|
2026-05-17 18:18:00 +03:00
|
|
|
|
/** 5 статусов воронки (редизайн 2026-05-17). */
|
2026-05-16 19:58:33 +03:00
|
|
|
|
const STATUS_OPTIONS: { value: string; title: string }[] = [
|
2026-05-17 18:18:00 +03:00
|
|
|
|
{ value: 'new', title: 'Новая сделка' },
|
2026-05-16 19:58:33 +03:00
|
|
|
|
{ value: 'viewed', title: 'Просмотрено' },
|
2026-05-17 18:18:00 +03:00
|
|
|
|
{ value: 'in_progress', title: 'В работе' },
|
|
|
|
|
|
{ value: 'won', title: 'Сделка' },
|
|
|
|
|
|
{ value: 'lost', title: 'Не реализовано' },
|
2026-05-16 19:58:33 +03:00
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
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),
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-06-17 05:17:12 +03:00
|
|
|
|
const allMapped = computed(() => props.statuses.length > 0 && props.statuses.every((s) => !!selection[s.status_ru]));
|
2026-05-16 19:58:33 +03:00
|
|
|
|
|
|
|
|
|
|
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">
|
|
|
|
|
|
<v-card>
|
|
|
|
|
|
<v-card-title class="text-h6">Маппинг неизвестных статусов</v-card-title>
|
|
|
|
|
|
<v-card-text>
|
|
|
|
|
|
<p class="text-body-2 text-medium-emphasis mb-4">
|
2026-06-17 05:17:12 +03:00
|
|
|
|
Эти статусы из CSV не входят в стандартную воронку. Выберите соответствие — повторный импорт
|
|
|
|
|
|
применит маппинг автоматически.
|
2026-05-16 19:58:33 +03:00
|
|
|
|
</p>
|
2026-06-17 05:17:12 +03:00
|
|
|
|
<div v-for="status in statuses" :key="status.id" class="d-flex align-center ga-3 mb-3">
|
2026-05-16 19:58:33 +03:00
|
|
|
|
<div class="flex-grow-1">
|
|
|
|
|
|
<strong>{{ status.status_ru }}</strong>
|
2026-06-17 05:17:12 +03:00
|
|
|
|
<span class="text-caption text-medium-emphasis ml-2"> ({{ status.occurrences }} шт.) </span>
|
2026-05-16 19:58:33 +03:00
|
|
|
|
</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>
|