117 lines
4.3 KiB
Vue
117 lines
4.3 KiB
Vue
<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';
|
||
|
||
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">
|
||
<v-card>
|
||
<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>
|