Initial commit MKD fixes
This commit is contained in:
626
services/domaGraphQLClient.ts
Executable file
626
services/domaGraphQLClient.ts
Executable file
@@ -0,0 +1,626 @@
|
||||
/**
|
||||
* GraphQL клиент для работы с Doma AI API
|
||||
* Документация: https://developers.doma.ai/ru/docs/api/about
|
||||
*/
|
||||
|
||||
import { settingsService, DomaAISettings } from './settingsService';
|
||||
import { getAuthToken } from './apiClient';
|
||||
|
||||
export interface DomaGraphQLResponse<T> {
|
||||
data?: T;
|
||||
errors?: Array<{
|
||||
message: string;
|
||||
extensions?: any;
|
||||
}>;
|
||||
}
|
||||
|
||||
export interface DomaAuthResponse {
|
||||
authenticateUserWithPassword: {
|
||||
token: string;
|
||||
item: {
|
||||
id: string;
|
||||
name: string;
|
||||
email?: string;
|
||||
phone?: string;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export interface DomaTicket {
|
||||
id: string;
|
||||
number: string;
|
||||
status: {
|
||||
id: string;
|
||||
name: string;
|
||||
type: string;
|
||||
};
|
||||
details?: string;
|
||||
description?: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
deadline?: string;
|
||||
property?: {
|
||||
id: string;
|
||||
address: string;
|
||||
unitName?: string;
|
||||
};
|
||||
client?: {
|
||||
id: string;
|
||||
name: string;
|
||||
phone?: string;
|
||||
};
|
||||
assignee?: {
|
||||
id: string;
|
||||
name: string;
|
||||
};
|
||||
category?: {
|
||||
id: string;
|
||||
name: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface DomaTicketsResponse {
|
||||
objs: DomaTicket[];
|
||||
meta?: {
|
||||
count: number;
|
||||
};
|
||||
}
|
||||
|
||||
class DomaGraphQLClient {
|
||||
private apiUrl: string | null = null;
|
||||
private token: string | null = null;
|
||||
private tokenStorageKey = 'doma_ai_token';
|
||||
|
||||
constructor() {
|
||||
// Восстанавливаем токен из localStorage, если есть
|
||||
const storedToken = localStorage.getItem(this.tokenStorageKey);
|
||||
if (storedToken) {
|
||||
this.token = storedToken;
|
||||
}
|
||||
|
||||
// Настраиваем URL бэкенда, через который проксируем запросы в Doma AI
|
||||
this.updateApiUrl();
|
||||
}
|
||||
|
||||
/**
|
||||
* Обновляет URL API до нашего backend-прокси, а не напрямую до Doma AI
|
||||
*/
|
||||
private updateApiUrl(): void {
|
||||
const backendBaseUrl = import.meta.env.VITE_API_BASE_URL;
|
||||
|
||||
if (backendBaseUrl) {
|
||||
// Все запросы к Doma AI теперь идут через backend-прокси
|
||||
this.apiUrl = `${backendBaseUrl.replace(/\/$/, '')}/doma/graphql`;
|
||||
return;
|
||||
}
|
||||
|
||||
this.apiUrl = null;
|
||||
console.warn(
|
||||
'[DomaGraphQLClient] VITE_API_BASE_URL не задан. ' +
|
||||
'Настройте URL бэкенда в переменных окружения фронтенда.'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Устанавливает настройки API из настроек интеграций
|
||||
*/
|
||||
updateSettings(): void {
|
||||
this.updateApiUrl();
|
||||
}
|
||||
|
||||
/**
|
||||
* Аутентификация пользователя через email и пароль
|
||||
* Если параметры не переданы, пытается получить из настроек
|
||||
*/
|
||||
async authenticate(email?: string, password?: string): Promise<string> {
|
||||
// Если параметры не переданы, пытаемся получить из настроек
|
||||
if (!email || !password) {
|
||||
const settings = settingsService.getDomaAISettings();
|
||||
if (settings?.email && settings?.password) {
|
||||
email = settings.email;
|
||||
password = settings.password;
|
||||
} else {
|
||||
throw new Error('Учетные данные не указаны. Укажите email и пароль в настройках интеграций.');
|
||||
}
|
||||
}
|
||||
|
||||
// Обновляем URL перед авторизацией
|
||||
this.updateApiUrl();
|
||||
const mutation = `
|
||||
mutation AuthenticateUser($email: String!, $password: String!) {
|
||||
authenticateUserWithPassword(email: $email, password: $password) {
|
||||
token
|
||||
item {
|
||||
id
|
||||
name
|
||||
email
|
||||
phone
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const response = await this.request<DomaAuthResponse>(mutation, {
|
||||
email,
|
||||
password,
|
||||
});
|
||||
|
||||
if (response.errors) {
|
||||
throw new Error(response.errors[0]?.message || 'Ошибка аутентификации');
|
||||
}
|
||||
|
||||
if (response.data?.authenticateUserWithPassword?.token) {
|
||||
this.token = response.data.authenticateUserWithPassword.token;
|
||||
localStorage.setItem(this.tokenStorageKey, this.token);
|
||||
return this.token;
|
||||
}
|
||||
|
||||
throw new Error('Не удалось получить токен авторизации');
|
||||
}
|
||||
|
||||
/**
|
||||
* Аутентификация через телефон и пароль
|
||||
* Если параметры не переданы, пытается получить из настроек
|
||||
*/
|
||||
async authenticateWithPhone(phone?: string, password?: string): Promise<string> {
|
||||
// Если параметры не переданы, пытаемся получить из настроек
|
||||
if (!phone || !password) {
|
||||
const settings = settingsService.getDomaAISettings();
|
||||
if (settings?.phone && settings?.password) {
|
||||
phone = settings.phone;
|
||||
password = settings.password;
|
||||
} else {
|
||||
throw new Error('Учетные данные не указаны. Укажите телефон и пароль в настройках интеграций.');
|
||||
}
|
||||
}
|
||||
|
||||
// Обновляем URL перед авторизацией
|
||||
this.updateApiUrl();
|
||||
const mutation = `
|
||||
mutation AuthenticateUserWithPhone($phone: String!, $password: String!) {
|
||||
authenticateUserWithPhoneAndPassword(phone: $phone, password: $password) {
|
||||
token
|
||||
item {
|
||||
id
|
||||
name
|
||||
email
|
||||
phone
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
interface PhoneAuthResponse {
|
||||
authenticateUserWithPhoneAndPassword: {
|
||||
token: string;
|
||||
item: {
|
||||
id: string;
|
||||
name: string;
|
||||
email?: string;
|
||||
phone?: string;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
const response = await this.request<PhoneAuthResponse>(mutation, {
|
||||
phone,
|
||||
password,
|
||||
});
|
||||
|
||||
if (response.errors) {
|
||||
throw new Error(response.errors[0]?.message || 'Ошибка аутентификации');
|
||||
}
|
||||
|
||||
if (response.data?.authenticateUserWithPhoneAndPassword?.token) {
|
||||
this.token = response.data.authenticateUserWithPhoneAndPassword.token;
|
||||
localStorage.setItem(this.tokenStorageKey, this.token);
|
||||
return this.token;
|
||||
}
|
||||
|
||||
throw new Error('Не удалось получить токен авторизации');
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверка текущего пользователя
|
||||
*/
|
||||
async getAuthenticatedUser() {
|
||||
const query = `
|
||||
query {
|
||||
authenticatedUser {
|
||||
id
|
||||
name
|
||||
email
|
||||
phone
|
||||
type
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const response = await this.request(query);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Получение списка заявок (tickets)
|
||||
* В Doma AI заявки могут называться Ticket, ServiceRequest или аналогично
|
||||
*/
|
||||
async getTickets(filters?: {
|
||||
status?: string;
|
||||
assignee?: string;
|
||||
property?: string;
|
||||
limit?: number;
|
||||
skip?: number;
|
||||
}): Promise<DomaTicket[]> {
|
||||
// Пробуем разные возможные названия типов заявок в Doma AI
|
||||
// В зависимости от вашей схемы это может быть Ticket, ServiceRequest, Request и т.д.
|
||||
|
||||
const query = `
|
||||
query GetTickets($where: TicketWhereInput, $first: Int, $skip: Int) {
|
||||
objs: tickets(where: $where, first: $first, skip: $skip, orderBy: createdAt_DESC) {
|
||||
id
|
||||
number
|
||||
status {
|
||||
id
|
||||
name
|
||||
type
|
||||
}
|
||||
details
|
||||
description
|
||||
createdAt
|
||||
updatedAt
|
||||
deadline
|
||||
property {
|
||||
id
|
||||
address
|
||||
unitName
|
||||
}
|
||||
client {
|
||||
id
|
||||
name
|
||||
phone
|
||||
}
|
||||
assignee {
|
||||
id
|
||||
name
|
||||
}
|
||||
category {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const variables: any = {
|
||||
first: filters?.limit || 100,
|
||||
skip: filters?.skip || 0,
|
||||
};
|
||||
|
||||
if (filters) {
|
||||
variables.where = {};
|
||||
if (filters.status) {
|
||||
variables.where.status = { id: filters.status };
|
||||
}
|
||||
if (filters.assignee) {
|
||||
variables.where.assignee = { id: filters.assignee };
|
||||
}
|
||||
if (filters.property) {
|
||||
variables.where.property = { id: filters.property };
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await this.request<{ objs: DomaTicket[] }>(query, variables);
|
||||
|
||||
if (response.errors) {
|
||||
console.warn('[DomaGraphQLClient] Ошибка при получении заявок:', response.errors);
|
||||
// Пробуем альтернативный запрос
|
||||
return await this.getTicketsAlternative(filters);
|
||||
}
|
||||
|
||||
return response.data?.objs || [];
|
||||
} catch (error) {
|
||||
console.error('[DomaGraphQLClient] Ошибка при запросе заявок:', error);
|
||||
// Пробуем альтернативный запрос
|
||||
return await this.getTicketsAlternative(filters);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Альтернативный запрос заявок (если основная схема не работает)
|
||||
*/
|
||||
private async getTicketsAlternative(filters?: any): Promise<DomaTicket[]> {
|
||||
// Пробуем запрос через ServiceRequest или другие возможные типы
|
||||
const alternativeQueries = [
|
||||
// Вариант 1: ServiceRequest
|
||||
`
|
||||
query GetServiceRequests($where: ServiceRequestWhereInput, $first: Int, $skip: Int) {
|
||||
objs: serviceRequests(where: $where, first: $first, skip: $skip, orderBy: createdAt_DESC) {
|
||||
id
|
||||
number
|
||||
status {
|
||||
id
|
||||
name
|
||||
type
|
||||
}
|
||||
details
|
||||
description
|
||||
createdAt
|
||||
updatedAt
|
||||
deadline
|
||||
property {
|
||||
id
|
||||
address
|
||||
unitName
|
||||
}
|
||||
client {
|
||||
id
|
||||
name
|
||||
phone
|
||||
}
|
||||
assignee {
|
||||
id
|
||||
name
|
||||
}
|
||||
category {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
// Вариант 2: Request
|
||||
`
|
||||
query GetRequests($where: RequestWhereInput, $first: Int, $skip: Int) {
|
||||
objs: requests(where: $where, first: $first, skip: $skip, orderBy: createdAt_DESC) {
|
||||
id
|
||||
number
|
||||
status {
|
||||
id
|
||||
name
|
||||
type
|
||||
}
|
||||
details
|
||||
description
|
||||
createdAt
|
||||
updatedAt
|
||||
deadline
|
||||
property {
|
||||
id
|
||||
address
|
||||
unitName
|
||||
}
|
||||
client {
|
||||
id
|
||||
name
|
||||
phone
|
||||
}
|
||||
assignee {
|
||||
id
|
||||
name
|
||||
}
|
||||
category {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
for (const query of alternativeQueries) {
|
||||
try {
|
||||
const variables: any = {
|
||||
first: filters?.limit || 100,
|
||||
skip: filters?.skip || 0,
|
||||
};
|
||||
|
||||
if (filters) {
|
||||
variables.where = {};
|
||||
if (filters.status) {
|
||||
variables.where.status = { id: filters.status };
|
||||
}
|
||||
}
|
||||
|
||||
const response = await this.request<{ objs: DomaTicket[] }>(query, variables);
|
||||
|
||||
if (response.data?.objs) {
|
||||
return response.data.objs;
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('[DomaGraphQLClient] Альтернативный запрос не сработал:', error);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Базовый метод для выполнения GraphQL запросов
|
||||
*/
|
||||
private async request<T>(
|
||||
query: string,
|
||||
variables?: Record<string, any>
|
||||
): Promise<DomaGraphQLResponse<T>> {
|
||||
if (!this.apiUrl) {
|
||||
throw new Error('VITE_DOMA_AI_API_URL не настроен. Укажите URL вашего инстанса Doma AI.');
|
||||
}
|
||||
|
||||
const headers: HeadersInit = {
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
const portalToken = getAuthToken();
|
||||
if (portalToken) {
|
||||
(headers as Record<string, string>)['Authorization'] = `Bearer ${portalToken}`;
|
||||
}
|
||||
|
||||
// Важно: авторизационный токен к Doma AI больше не отправляем из браузера напрямую.
|
||||
// Вместо этого передаём его в config.body на наш backend-прокси.
|
||||
|
||||
// Читаем актуальные настройки интеграции
|
||||
const settings = settingsService.getDomaAISettings();
|
||||
|
||||
const config = {
|
||||
apiUrl: settings?.apiUrl,
|
||||
token: settings?.token || this.token || null,
|
||||
};
|
||||
|
||||
try {
|
||||
const bodyPayload = {
|
||||
query,
|
||||
variables: variables || {},
|
||||
config,
|
||||
};
|
||||
|
||||
console.log('[DomaGraphQLClient] Запрос к Doma AI', {
|
||||
url: this.apiUrl,
|
||||
hasToken: !!this.token,
|
||||
// Лучше не логировать полный запрос, чтобы не засорять консоль,
|
||||
// показываем только первые символы
|
||||
queryPreview: query.slice(0, 100),
|
||||
});
|
||||
|
||||
const response = await fetch(this.apiUrl, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify(bodyPayload),
|
||||
});
|
||||
|
||||
const rawText = await response.text();
|
||||
|
||||
console.log('[DomaGraphQLClient] Ответ Doma AI', {
|
||||
status: response.status,
|
||||
ok: response.ok,
|
||||
length: rawText.length,
|
||||
preview: rawText.slice(0, 300),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
if (!rawText) {
|
||||
// Пустой ответ от Doma AI
|
||||
return {} as DomaGraphQLResponse<T>;
|
||||
}
|
||||
|
||||
const result: DomaGraphQLResponse<T> = JSON.parse(rawText);
|
||||
|
||||
// Если получили ошибку авторизации, очищаем токен
|
||||
if (result.errors?.some(e => e.message.includes('auth') || e.message.includes('unauthorized'))) {
|
||||
this.token = null;
|
||||
localStorage.removeItem(this.tokenStorageKey);
|
||||
throw new Error('Требуется повторная авторизация');
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error('[DomaGraphQLClient] Ошибка запроса:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Установка токена вручную (если токен получен извне)
|
||||
*/
|
||||
setToken(token: string): void {
|
||||
this.token = token;
|
||||
localStorage.setItem(this.tokenStorageKey, token);
|
||||
}
|
||||
|
||||
/**
|
||||
* Очистка токена (выход)
|
||||
*/
|
||||
clearToken(): void {
|
||||
this.token = null;
|
||||
localStorage.removeItem(this.tokenStorageKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверка наличия токена
|
||||
*/
|
||||
hasToken(): boolean {
|
||||
return !!this.token;
|
||||
}
|
||||
|
||||
/**
|
||||
* Обновление заявки (ticket) в Doma AI
|
||||
* @param ticketId - ID заявки в Doma AI (UUID)
|
||||
* @param data - Данные для обновления
|
||||
*/
|
||||
async updateTicket(
|
||||
ticketId: string,
|
||||
data: {
|
||||
status?: { id?: string; type?: string };
|
||||
statusReason?: string;
|
||||
deferredUntil?: string;
|
||||
lastCommentWithResidentTypeAt?: string;
|
||||
}
|
||||
): Promise<{ id: string; number: string }> {
|
||||
const mutation = `
|
||||
mutation UpdateTicket($id: ID!, $data: TicketUpdateInput!) {
|
||||
obj: updateTicket(id: $id, data: $data) {
|
||||
id
|
||||
number
|
||||
status {
|
||||
id
|
||||
name
|
||||
type
|
||||
}
|
||||
updatedAt
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const variables: any = {
|
||||
id: ticketId,
|
||||
data: {},
|
||||
};
|
||||
|
||||
// Маппинг локальных статусов в статусы Doma.AI
|
||||
const statusTypeMap: Record<string, string> = {
|
||||
new: 'new_or_reopened',
|
||||
in_progress: 'processing',
|
||||
deferred: 'deferred',
|
||||
done: 'completed',
|
||||
canceled: 'canceled',
|
||||
};
|
||||
|
||||
if (data.status?.type) {
|
||||
const domaStatusType = statusTypeMap[data.status.type] || data.status.type;
|
||||
variables.data.status = { type: domaStatusType };
|
||||
} else if (data.status?.id) {
|
||||
variables.data.status = { id: data.status.id };
|
||||
}
|
||||
|
||||
if (data.statusReason) {
|
||||
variables.data.statusReason = data.statusReason;
|
||||
}
|
||||
|
||||
if (data.deferredUntil) {
|
||||
variables.data.deferredUntil = data.deferredUntil;
|
||||
}
|
||||
|
||||
if (data.lastCommentWithResidentTypeAt) {
|
||||
variables.data.lastCommentWithResidentTypeAt = data.lastCommentWithResidentTypeAt;
|
||||
}
|
||||
|
||||
// Устанавливаем дату обновления статуса
|
||||
variables.data.statusUpdatedAt = new Date().toISOString();
|
||||
|
||||
const response = await this.request<{ obj: { id: string; number: string } }>(mutation, variables);
|
||||
|
||||
if (response.errors) {
|
||||
throw new Error(response.errors[0]?.message || 'Ошибка при обновлении заявки');
|
||||
}
|
||||
|
||||
if (!response.data?.obj) {
|
||||
throw new Error('Не удалось обновить заявку');
|
||||
}
|
||||
|
||||
return response.data.obj;
|
||||
}
|
||||
}
|
||||
|
||||
export const domaGraphQLClient = new DomaGraphQLClient();
|
||||
Reference in New Issue
Block a user