Some checks failed
Telegram Mini App Shop Builder / Compute version metadata (push) Has been cancelled
Telegram Mini App Shop Builder / Run Frontend tests (push) Has been cancelled
Telegram Mini App Shop Builder / Run Backend tests (push) Has been cancelled
Telegram Mini App Shop Builder / Run PHP_CodeSniffer (push) Has been cancelled
Telegram Mini App Shop Builder / Build module. (push) Has been cancelled
Telegram Mini App Shop Builder / release (push) Has been cancelled
707 lines
24 KiB
Vue
707 lines
24 KiB
Vue
<template>
|
||
<div>
|
||
<DataTable
|
||
:value="customers"
|
||
:loading="loading"
|
||
paginator
|
||
:rows="20"
|
||
:rowsPerPageOptions="[10, 20, 50, 100]"
|
||
:sortField="lazyParams.sortField"
|
||
:sortOrder="lazyParams.sortOrder"
|
||
showGridlines
|
||
stripedRows
|
||
size="small"
|
||
removableSort
|
||
:globalFilterFields="['telegram_user_id', 'username', 'first_name', 'last_name', 'language_code']"
|
||
v-model:filters="filters"
|
||
filterDisplay="menu"
|
||
:lazy="true"
|
||
:totalRecords="totalRecords"
|
||
@page="onPage"
|
||
@sort="onSort"
|
||
@filter="onFilter"
|
||
paginatorTemplate="FirstPageLink PrevPageLink PageLinks NextPageLink LastPageLink CurrentPageReport RowsPerPageDropdown"
|
||
:currentPageReportTemplate="`Показано {first} - {last} из {totalRecords} записей`"
|
||
>
|
||
<template #header>
|
||
<div class="tw:flex tw:flex-wrap tw:items-center tw:justify-between tw:gap-2">
|
||
<div class="tw:flex tw:items-center tw:gap-2">
|
||
<Button
|
||
icon="fa fa-columns"
|
||
:label="`Колонки (${selectedColumns.length}/${columns.length})`"
|
||
@click="toggleColumnsPanel"
|
||
size="small"
|
||
/>
|
||
<OverlayPanel ref="columnsPanel">
|
||
<div class="tw:flex tw:flex-col tw:gap-2 tw:min-w-[200px]">
|
||
<div class="tw:flex tw:gap-2 tw:mb-2">
|
||
<Button
|
||
label="Выбрать все"
|
||
size="small"
|
||
severity="secondary"
|
||
outlined
|
||
@click="selectAllColumns"
|
||
class="tw:flex-1"
|
||
/>
|
||
<Button
|
||
label="Снять все"
|
||
size="small"
|
||
severity="secondary"
|
||
outlined
|
||
@click="deselectAllColumns"
|
||
class="tw:flex-1"
|
||
/>
|
||
</div>
|
||
<div
|
||
v-for="col in columns"
|
||
:key="col.field"
|
||
class="tw:flex tw:items-center tw:gap-2"
|
||
>
|
||
<Checkbox
|
||
:inputId="col.field"
|
||
:modelValue="selectedColumns.some(c => c.field === col.field)"
|
||
@update:modelValue="(val) => toggleColumn(col, val)"
|
||
:binary="true"
|
||
/>
|
||
<label :for="col.field" class="tw:cursor-pointer">{{ col.header }}</label>
|
||
</div>
|
||
</div>
|
||
</OverlayPanel>
|
||
<Button icon="fa fa-refresh" @click="loadCustomers" v-tooltip.top="'Обновить таблицу'"
|
||
size="small"/>
|
||
<Button icon="fa fa-times-circle" label="Сбросить фильтры" @click="resetFilters"
|
||
v-tooltip.top="'Сбросить все фильтры'" size="small"/>
|
||
</div>
|
||
|
||
<IconField>
|
||
<InputIcon class="fa fa-search"/>
|
||
<InputText v-model="globalSearchValue" placeholder="Поиск по таблице..."
|
||
@input="onGlobalSearch"/>
|
||
</IconField>
|
||
</div>
|
||
</template>
|
||
|
||
<Column header="Действия" :exportable="false" headerStyle="width: 5rem">
|
||
<template #body="{ data }">
|
||
<Button
|
||
icon="fa fa-paper-plane"
|
||
severity="secondary"
|
||
text
|
||
rounded
|
||
@click="openMessageDialog(data)"
|
||
v-tooltip.top="'Отправить сообщение пользователю в Telegram'"
|
||
/>
|
||
</template>
|
||
</Column>
|
||
|
||
<Column v-for="col in selectedColumns" :key="col.field" :field="col.field"
|
||
:sortable="col.sortable" :dataType="col.dataType"
|
||
:showFilterMenu="col.filterable !== false">
|
||
<template #header>
|
||
<div class="tw:flex tw:items-center tw:gap-1">
|
||
<span>{{ col.header }}</span>
|
||
<i
|
||
v-if="col.help"
|
||
class="fa fa-question-circle tw:text-gray-400 tw:cursor-help"
|
||
v-tooltip.top="col.help"
|
||
></i>
|
||
</div>
|
||
</template>
|
||
<template #body="{ data }">
|
||
<template v-if="col.field === 'id'">{{ data.id }}</template>
|
||
<template v-else-if="col.field === 'telegram_user_id'">
|
||
{{ data.telegram_user_id }}
|
||
</template>
|
||
<template v-else-if="col.field === 'tracking_id'">
|
||
<code>{{ data.tracking_id }}</code>
|
||
</template>
|
||
<template v-else-if="col.field === 'username'">
|
||
<div class="tw:flex tw:items-center tw:gap-2">
|
||
<div v-if="data.photo_url" class="tw:relative">
|
||
<img
|
||
:src="data.photo_url"
|
||
:alt="data.username || 'Avatar'"
|
||
class="tw:w-6 tw:h-6 tw:rounded-full tw:object-cover"
|
||
@error="handleImageError"
|
||
/>
|
||
</div>
|
||
<i v-else class="fa fa-user tw:text-gray-400"></i>
|
||
<span v-if="data.username">@{{ data.username }}</span>
|
||
<span v-else>—</span>
|
||
</div>
|
||
</template>
|
||
<template v-else-if="col.field === 'first_name'">{{ data.first_name || '—' }}</template>
|
||
<template v-else-if="col.field === 'last_name'">{{ data.last_name || '—' }}</template>
|
||
<template v-else-if="col.field === 'language_code'">
|
||
<span v-if="data.language_code">
|
||
<i class="fa fa-globe"></i> {{ data.language_code.toUpperCase() }}
|
||
</span>
|
||
<span v-else>—</span>
|
||
</template>
|
||
<template v-else-if="col.field === 'is_premium'">
|
||
<i v-if="data.is_premium" class="fa fa-star" v-tooltip.top="'Премиум пользователь'"></i>
|
||
<span v-else>—</span>
|
||
</template>
|
||
<template v-else-if="col.field === 'orders_count'">
|
||
<span>{{ data.orders_count }}</span>
|
||
</template>
|
||
<template v-else-if="col.field === 'oc_customer_id'">
|
||
<span v-if="data.oc_customer_id">{{ data.oc_customer_id }}</span>
|
||
<span v-else>—</span>
|
||
</template>
|
||
<template v-else-if="col.field === 'last_seen_at'">
|
||
<span v-if="data.last_seen_at">
|
||
<i class="fa fa-clock-o"></i> {{ formatDate(data.last_seen_at) }}
|
||
</span>
|
||
<span v-else>—</span>
|
||
</template>
|
||
<template v-else-if="col.field === 'privacy_consented_at'">
|
||
<span v-if="data.privacy_consented_at">
|
||
<i class="fa fa-clock-o"></i> {{ formatDate(data.privacy_consented_at) }}
|
||
</span>
|
||
<span v-else>—</span>
|
||
</template>
|
||
<template v-else-if="col.field === 'created_at'">
|
||
<i class="fa fa-calendar"></i> {{ formatDate(data.created_at) }}
|
||
</template>
|
||
</template>
|
||
<template #filter="{ filterModel }">
|
||
<template v-if="col.field === 'telegram_user_id'">
|
||
<InputText v-model="filterModel.value" type="text" placeholder="Поиск по ID"
|
||
class="p-column-filter"/>
|
||
</template>
|
||
<template v-else-if="col.field === 'username'">
|
||
<InputText v-model="filterModel.value" type="text"
|
||
placeholder="Поиск по имени пользователя" class="p-column-filter"/>
|
||
</template>
|
||
<template v-else-if="col.field === 'first_name'">
|
||
<InputText v-model="filterModel.value" type="text" placeholder="Поиск по имени"
|
||
class="p-column-filter"/>
|
||
</template>
|
||
<template v-else-if="col.field === 'last_name'">
|
||
<InputText v-model="filterModel.value" type="text" placeholder="Поиск по фамилии"
|
||
class="p-column-filter"/>
|
||
</template>
|
||
<template
|
||
v-else-if="['last_seen_at', 'created_at', 'privacy_consented_at'].includes(col.field)">
|
||
<DatePicker v-model="filterModel.value" dateFormat="dd.mm.yy" placeholder="dd.mm.yyyy"/>
|
||
</template>
|
||
<template v-else-if="col.field === 'orders_count'">
|
||
<InputNumber v-model="filterModel.value"/>
|
||
</template>
|
||
<template v-else-if="col.field === 'is_premium'">
|
||
<Dropdown
|
||
v-model="filterModel.value"
|
||
:options="premiumFilterOptions"
|
||
optionLabel="label"
|
||
optionValue="value"
|
||
placeholder="Любой"
|
||
class="p-column-filter"
|
||
/>
|
||
</template>
|
||
</template>
|
||
</Column>
|
||
|
||
<template #empty>
|
||
<div style="text-align: center; padding: 2rem;">
|
||
<i class="fa fa-users" style="font-size: 3rem; color: #ccc; margin-bottom: 1rem;"></i>
|
||
<div>Нет данных о кастомерах</div>
|
||
<div style="font-size: 0.9rem; color: #999; margin-top: 0.5rem;">
|
||
Пользователи появятся здесь после первого входа в Telegram Mini App
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<template #loading>
|
||
<div style="text-align: center; padding: 2rem;">
|
||
<i class="fa fa-spinner fa-spin" style="font-size: 2rem;"></i>
|
||
<div style="margin-top: 1rem;">Загрузка данных...</div>
|
||
</div>
|
||
</template>
|
||
</DataTable>
|
||
|
||
<Dialog
|
||
v-model:visible="showMessageDialog"
|
||
modal
|
||
header="Отправить сообщение"
|
||
:style="{ width: '500px' }"
|
||
:closable="true"
|
||
>
|
||
<div style="margin-bottom: 1rem;">
|
||
<div style="margin-bottom: 0.5rem; font-weight: 600;">
|
||
Получатель:
|
||
</div>
|
||
<div v-if="selectedCustomer">
|
||
<div v-if="selectedCustomer.username">
|
||
<i class="fa fa-user"></i> @{{ selectedCustomer.username }}
|
||
</div>
|
||
<div v-if="selectedCustomer.first_name || selectedCustomer.last_name">
|
||
{{ selectedCustomer.first_name }} {{ selectedCustomer.last_name }}
|
||
</div>
|
||
<div style="font-size: 0.9rem; color: #666;">
|
||
ID: {{ selectedCustomer.telegram_user_id }}
|
||
</div>
|
||
<div v-if="!selectedCustomer.allows_write_to_pm"
|
||
style="color: #f59e0b; margin-top: 0.5rem;">
|
||
<i class="fa fa-exclamation-triangle"></i> Пользователь не разрешил писать ему в PM
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div style="margin-bottom: 1rem;">
|
||
<label for="message" style="display: block; margin-bottom: 0.5rem; font-weight: 600;">
|
||
Сообщение:
|
||
</label>
|
||
<Textarea
|
||
id="message"
|
||
v-model="messageText"
|
||
:rows="5"
|
||
:disabled="!selectedCustomer || !selectedCustomer.allows_write_to_pm"
|
||
placeholder="Введите текст сообщения..."
|
||
style="width: 100%;"
|
||
/>
|
||
</div>
|
||
|
||
<template #footer>
|
||
<Button
|
||
label="Отмена"
|
||
icon="fa fa-times"
|
||
severity="secondary"
|
||
@click="closeMessageDialog"
|
||
:disabled="sendingMessage"
|
||
/>
|
||
<Button
|
||
label="Отправить"
|
||
icon="fa fa-paper-plane"
|
||
@click="sendMessage"
|
||
:loading="sendingMessage"
|
||
:disabled="!messageText || !selectedCustomer || !selectedCustomer.allows_write_to_pm"
|
||
/>
|
||
</template>
|
||
</Dialog>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup>
|
||
import {onMounted, ref} from 'vue';
|
||
import {FilterMatchMode, FilterOperator} from '@primevue/core/api';
|
||
import DataTable from 'primevue/datatable';
|
||
import Column from 'primevue/column';
|
||
import DatePicker from 'primevue/datepicker';
|
||
import Dropdown from 'primevue/dropdown';
|
||
import InputText from 'primevue/inputtext';
|
||
import Dialog from 'primevue/dialog';
|
||
import Textarea from 'primevue/textarea';
|
||
import OverlayPanel from 'primevue/overlaypanel';
|
||
import Checkbox from 'primevue/checkbox';
|
||
import Button from 'primevue/button';
|
||
import InputNumber from 'primevue/inputnumber';
|
||
import {apiPost} from '@/utils/http.js';
|
||
import {IconField, InputIcon, useToast} from 'primevue';
|
||
|
||
const toast = useToast();
|
||
const customers = ref([]);
|
||
const loading = ref(false);
|
||
const totalRecords = ref(0);
|
||
const showMessageDialog = ref(false);
|
||
const selectedCustomer = ref(null);
|
||
const messageText = ref('');
|
||
const sendingMessage = ref(false);
|
||
|
||
const columns = ref([
|
||
{field: 'id', header: '№', sortable: true, filterable: false, visible: true},
|
||
{
|
||
field: 'telegram_user_id',
|
||
header: 'ID в Telegram',
|
||
sortable: true,
|
||
filterable: true,
|
||
visible: false
|
||
},
|
||
{
|
||
field: 'tracking_id',
|
||
header: 'Tracking ID',
|
||
sortable: false,
|
||
filterable: true,
|
||
visible: false,
|
||
help: 'Tracking ID это публичный уникальный идентификатор покупателя, используется в рекламных кампаниях для отслеживания активности.',
|
||
},
|
||
{field: 'username', header: 'Имя пользователя', sortable: true, filterable: true, visible: true},
|
||
{field: 'first_name', header: 'Имя', sortable: true, filterable: true, visible: true},
|
||
{field: 'last_name', header: 'Фамилия', sortable: true, filterable: true, visible: true},
|
||
{
|
||
field: 'language_code',
|
||
header: 'Язык интерфейса',
|
||
sortable: true,
|
||
filterable: false,
|
||
visible: false
|
||
},
|
||
{
|
||
field: 'orders_count',
|
||
header: 'Кол-во заказов',
|
||
sortable: true,
|
||
filterable: true,
|
||
dataType: 'numeric',
|
||
visible: true,
|
||
help: 'Общее количество Telegram заказов за всё время',
|
||
},
|
||
{field: 'is_premium', header: 'Премиум статус', sortable: true, filterable: true, visible: true},
|
||
{
|
||
field: 'oc_customer_id',
|
||
header: 'ID покупателя',
|
||
sortable: true,
|
||
filterable: false,
|
||
visible: false
|
||
},
|
||
{
|
||
field: 'last_seen_at',
|
||
header: 'Последний визит',
|
||
sortable: true,
|
||
dataType: 'date',
|
||
filterable: true,
|
||
visible: true,
|
||
help: 'Показывает, когда пользователь последний раз открывал Telegram магазин.',
|
||
},
|
||
{
|
||
field: 'privacy_consented_at',
|
||
header: 'Согласие ПД',
|
||
sortable: true,
|
||
dataType: 'date',
|
||
filterable: true,
|
||
visible: true,
|
||
help: 'Показывает, когда пользователь дал согласие на обработку персональных данных.',
|
||
},
|
||
{
|
||
field: 'created_at',
|
||
header: 'Дата регистрации',
|
||
sortable: true,
|
||
dataType: 'date',
|
||
filterable: true,
|
||
visible: true,
|
||
help: 'Показывает, когда пользователь впервые открыл Telegram магазин.',
|
||
},
|
||
]);
|
||
|
||
const selectedColumns = ref(columns.value.filter(col => col.visible !== false));
|
||
const columnsPanel = ref(null);
|
||
const globalSearchValue = ref('');
|
||
let searchTimeout = null;
|
||
|
||
function toggleColumnsPanel(event) {
|
||
columnsPanel.value.toggle(event);
|
||
}
|
||
|
||
function toggleColumn(col, checked) {
|
||
// Сохраняем порядок колонок из исходного массива columns
|
||
const selectedFields = new Set(selectedColumns.value.map(c => c.field));
|
||
|
||
if (checked) {
|
||
selectedFields.add(col.field);
|
||
} else {
|
||
selectedFields.delete(col.field);
|
||
}
|
||
|
||
// Пересоздаем массив, сохраняя исходный порядок из columns
|
||
selectedColumns.value = columns.value.filter(c => selectedFields.has(c.field));
|
||
}
|
||
|
||
function selectAllColumns() {
|
||
selectedColumns.value = [...columns.value];
|
||
}
|
||
|
||
function deselectAllColumns() {
|
||
selectedColumns.value = [];
|
||
}
|
||
|
||
const premiumFilterOptions = [
|
||
{label: 'Любой', value: null},
|
||
{label: 'Нет', value: false},
|
||
{label: 'Да', value: true},
|
||
];
|
||
|
||
const lazyParams = ref({
|
||
first: 0,
|
||
rows: 20,
|
||
page: 1,
|
||
sortField: 'last_seen_at',
|
||
sortOrder: -1,
|
||
filters: {},
|
||
});
|
||
|
||
const filters = ref({
|
||
global: {value: null, matchMode: FilterMatchMode.CONTAINS},
|
||
telegram_user_id: {
|
||
operator: FilterOperator.AND,
|
||
constraints: [{value: null, matchMode: FilterMatchMode.STARTS_WITH}]
|
||
},
|
||
username: {
|
||
operator: FilterOperator.AND,
|
||
constraints: [{value: null, matchMode: FilterMatchMode.CONTAINS}]
|
||
},
|
||
first_name: {
|
||
operator: FilterOperator.AND,
|
||
constraints: [{value: null, matchMode: FilterMatchMode.CONTAINS}]
|
||
},
|
||
last_name: {
|
||
operator: FilterOperator.AND,
|
||
constraints: [{value: null, matchMode: FilterMatchMode.CONTAINS}]
|
||
},
|
||
orders_count: {
|
||
operator: FilterOperator.AND,
|
||
constraints: [{value: null, matchMode: FilterMatchMode.EQUALS}],
|
||
},
|
||
is_premium: {
|
||
operator: FilterOperator.AND,
|
||
constraints: [{value: null, matchMode: FilterMatchMode.EQUALS}]
|
||
},
|
||
created_at: {
|
||
operator: FilterOperator.AND,
|
||
constraints: [{value: null, matchMode: FilterMatchMode.DATE_IS}]
|
||
},
|
||
last_seen_at: {
|
||
operator: FilterOperator.AND,
|
||
constraints: [{value: null, matchMode: FilterMatchMode.DATE_IS}]
|
||
},
|
||
privacy_consented_at: {
|
||
operator: FilterOperator.AND,
|
||
constraints: [{value: null, matchMode: FilterMatchMode.DATE_IS}]
|
||
},
|
||
});
|
||
|
||
function processFiltersForBackend(filtersObj) {
|
||
const processed = JSON.parse(JSON.stringify(filtersObj));
|
||
|
||
// Обрабатываем фильтры по датам
|
||
const dateFields = ['created_at', 'last_seen_at', 'privacy_consented_at'];
|
||
dateFields.forEach(field => {
|
||
if (processed[field] && processed[field].constraints) {
|
||
processed[field].constraints.forEach(constraint => {
|
||
if (constraint.value && ['dateIs', 'dateIsNot', 'dateBefore', 'dateAfter'].includes(constraint.matchMode)) {
|
||
// Преобразуем дату в формат YYYY-MM-DD, используя локальное время
|
||
const date = new Date(constraint.value);
|
||
const year = date.getFullYear();
|
||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||
const day = String(date.getDate()).padStart(2, '0');
|
||
constraint.value = `${year}-${month}-${day}`;
|
||
}
|
||
});
|
||
}
|
||
});
|
||
|
||
return processed;
|
||
}
|
||
|
||
async function loadCustomers(event = null) {
|
||
loading.value = true;
|
||
try {
|
||
const processedFilters = processFiltersForBackend(filters.value);
|
||
|
||
const params = {
|
||
page: lazyParams.value.page,
|
||
rows: lazyParams.value.rows,
|
||
sortField: lazyParams.value.sortField,
|
||
sortOrder: lazyParams.value.sortOrder === -1 ? 'DESC' : 'ASC',
|
||
filters: processedFilters,
|
||
};
|
||
|
||
const result = await apiPost('getTelegramCustomers', params);
|
||
if (result.success && result.data) {
|
||
// apiPost возвращает полный ответ сервера, а apiGet возвращает response.data.data
|
||
// Поэтому здесь нужно проверить, есть ли вложенный data
|
||
const responseData = result.data.data ? result.data.data : result.data;
|
||
customers.value = Array.isArray(responseData) ? responseData : (responseData.data || []);
|
||
totalRecords.value = responseData.totalRecords || 0;
|
||
} else {
|
||
toast.add({
|
||
severity: 'error',
|
||
summary: 'Ошибка',
|
||
detail: result.error || 'Не удалось загрузить данные',
|
||
life: 3000,
|
||
});
|
||
}
|
||
} catch (error) {
|
||
console.error('Ошибка при загрузке кастомеров:', error);
|
||
toast.add({
|
||
severity: 'error',
|
||
summary: 'Ошибка',
|
||
detail: 'Произошла ошибка при загрузке данных',
|
||
life: 3000,
|
||
});
|
||
} finally {
|
||
loading.value = false;
|
||
}
|
||
}
|
||
|
||
function onPage(event) {
|
||
lazyParams.value.first = event.first;
|
||
lazyParams.value.rows = event.rows;
|
||
lazyParams.value.page = event.page + 1;
|
||
loadCustomers(event);
|
||
}
|
||
|
||
function onSort(event) {
|
||
lazyParams.value.sortField = event.sortField;
|
||
lazyParams.value.sortOrder = event.sortOrder;
|
||
lazyParams.value.page = 1;
|
||
lazyParams.value.first = 0;
|
||
loadCustomers(event);
|
||
}
|
||
|
||
function onFilter(event) {
|
||
filters.value = event.filters;
|
||
lazyParams.value.page = 1;
|
||
lazyParams.value.first = 0;
|
||
loadCustomers(event);
|
||
}
|
||
|
||
function onGlobalSearch() {
|
||
// Обновляем глобальный фильтр
|
||
filters.value.global.value = globalSearchValue.value || null;
|
||
|
||
// Сбрасываем на первую страницу
|
||
lazyParams.value.page = 1;
|
||
lazyParams.value.first = 0;
|
||
|
||
// Debounce: очищаем предыдущий таймер
|
||
if (searchTimeout) {
|
||
clearTimeout(searchTimeout);
|
||
}
|
||
|
||
// Устанавливаем новый таймер для отправки запроса через 300ms после последнего ввода
|
||
searchTimeout = setTimeout(() => {
|
||
loadCustomers();
|
||
}, 800);
|
||
}
|
||
|
||
function resetFilters() {
|
||
globalSearchValue.value = '';
|
||
filters.value = {
|
||
global: {value: null, matchMode: FilterMatchMode.CONTAINS},
|
||
telegram_user_id: {
|
||
operator: FilterOperator.AND,
|
||
constraints: [{value: null, matchMode: FilterMatchMode.STARTS_WITH}]
|
||
},
|
||
username: {
|
||
operator: FilterOperator.AND,
|
||
constraints: [{value: null, matchMode: FilterMatchMode.CONTAINS}]
|
||
},
|
||
first_name: {
|
||
operator: FilterOperator.AND,
|
||
constraints: [{value: null, matchMode: FilterMatchMode.CONTAINS}]
|
||
},
|
||
last_name: {
|
||
operator: FilterOperator.AND,
|
||
constraints: [{value: null, matchMode: FilterMatchMode.CONTAINS}]
|
||
},
|
||
orders_count: {
|
||
operator: FilterOperator.AND,
|
||
constraints: [{value: null, matchMode: FilterMatchMode.EQUALS}]
|
||
},
|
||
is_premium: {
|
||
operator: FilterOperator.AND,
|
||
constraints: [{value: null, matchMode: FilterMatchMode.EQUALS}]
|
||
},
|
||
created_at: {
|
||
operator: FilterOperator.AND,
|
||
constraints: [{value: null, matchMode: FilterMatchMode.DATE_IS}]
|
||
},
|
||
last_seen_at: {
|
||
operator: FilterOperator.AND,
|
||
constraints: [{value: null, matchMode: FilterMatchMode.DATE_IS}]
|
||
},
|
||
privacy_consented_at: {
|
||
operator: FilterOperator.AND,
|
||
constraints: [{value: null, matchMode: FilterMatchMode.DATE_IS}]
|
||
},
|
||
};
|
||
lazyParams.value.page = 1;
|
||
lazyParams.value.first = 0;
|
||
loadCustomers();
|
||
}
|
||
|
||
function handleImageError(event) {
|
||
// Скрываем изображение при ошибке загрузки
|
||
event.target.style.display = 'none';
|
||
}
|
||
|
||
function formatDate(dateString) {
|
||
if (!dateString) return '—';
|
||
const date = new Date(dateString);
|
||
return date.toLocaleString('ru-RU', {
|
||
year: 'numeric',
|
||
month: '2-digit',
|
||
day: '2-digit',
|
||
hour: '2-digit',
|
||
minute: '2-digit',
|
||
});
|
||
}
|
||
|
||
function openMessageDialog(customer) {
|
||
selectedCustomer.value = customer;
|
||
messageText.value = '';
|
||
showMessageDialog.value = true;
|
||
}
|
||
|
||
function closeMessageDialog() {
|
||
showMessageDialog.value = false;
|
||
selectedCustomer.value = null;
|
||
messageText.value = '';
|
||
}
|
||
|
||
async function sendMessage() {
|
||
if (!selectedCustomer.value || !messageText.value.trim()) {
|
||
return;
|
||
}
|
||
|
||
if (!selectedCustomer.value.allows_write_to_pm) {
|
||
toast.add({
|
||
severity: 'warn',
|
||
summary: 'Предупреждение',
|
||
detail: 'Пользователь не разрешил писать ему в PM',
|
||
life: 3000,
|
||
});
|
||
return;
|
||
}
|
||
|
||
sendingMessage.value = true;
|
||
try {
|
||
const result = await apiPost('sendMessageToCustomer', {
|
||
id: selectedCustomer.value.id,
|
||
message: messageText.value.trim(),
|
||
});
|
||
|
||
if (result.success && result.data?.success) {
|
||
toast.add({
|
||
severity: 'success',
|
||
summary: 'Успешно',
|
||
detail: result.data?.message || 'Сообщение отправлено',
|
||
life: 3000,
|
||
});
|
||
closeMessageDialog();
|
||
} else {
|
||
toast.add({
|
||
severity: 'error',
|
||
summary: 'Ошибка',
|
||
detail: result.data?.error || result.error || 'Не удалось отправить сообщение',
|
||
life: 3000,
|
||
});
|
||
}
|
||
} catch (error) {
|
||
console.error('Ошибка при отправке сообщения:', error);
|
||
toast.add({
|
||
severity: 'error',
|
||
summary: 'Ошибка',
|
||
detail: 'Произошла ошибка при отправке сообщения',
|
||
life: 3000,
|
||
});
|
||
} finally {
|
||
sendingMessage.value = false;
|
||
}
|
||
}
|
||
|
||
onMounted(() => {
|
||
loadCustomers();
|
||
});
|
||
</script>
|
||
|
||
|