2043 lines
72 KiB
TypeScript
2043 lines
72 KiB
TypeScript
/**
|
||
* API Client для работы с Django Backend через Next.js API proxy
|
||
* Все запросы идут на /api/* и проксируются на backend
|
||
*/
|
||
|
||
interface LoginCredentials {
|
||
email?: string;
|
||
phone?: string;
|
||
password: string;
|
||
}
|
||
|
||
interface LoginResponse {
|
||
access: string;
|
||
refresh: string;
|
||
}
|
||
|
||
class ApiClient {
|
||
private tokenKey = 'access_token';
|
||
private refreshTokenKey = 'refresh_token';
|
||
private isRefreshing = false;
|
||
private refreshSubscribers: Array<(token: string) => void> = [];
|
||
|
||
private getToken(): string | null {
|
||
if (typeof window === 'undefined') return null;
|
||
return localStorage.getItem(this.tokenKey);
|
||
}
|
||
|
||
private getRefreshToken(): string | null {
|
||
if (typeof window === 'undefined') return null;
|
||
return localStorage.getItem(this.refreshTokenKey);
|
||
}
|
||
|
||
private setToken(token: string): void {
|
||
if (typeof window !== 'undefined') {
|
||
localStorage.setItem(this.tokenKey, token);
|
||
}
|
||
}
|
||
|
||
private setRefreshToken(token: string): void {
|
||
if (typeof window !== 'undefined') {
|
||
localStorage.setItem(this.refreshTokenKey, token);
|
||
}
|
||
}
|
||
|
||
clearToken(): void {
|
||
if (typeof window !== 'undefined') {
|
||
localStorage.removeItem(this.tokenKey);
|
||
localStorage.removeItem(this.refreshTokenKey);
|
||
}
|
||
}
|
||
|
||
private onRefreshed(token: string): void {
|
||
this.refreshSubscribers.forEach((callback) => callback(token));
|
||
this.refreshSubscribers = [];
|
||
}
|
||
|
||
private addRefreshSubscriber(callback: (token: string) => void): void {
|
||
this.refreshSubscribers.push(callback);
|
||
}
|
||
|
||
/**
|
||
* Обновление access token с помощью refresh token
|
||
*/
|
||
private async refreshAccessToken(): Promise<string> {
|
||
const refreshToken = this.getRefreshToken();
|
||
if (!refreshToken) {
|
||
throw new Error('No refresh token available');
|
||
}
|
||
|
||
const response = await fetch('/api/auth/token/refresh/', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
},
|
||
body: JSON.stringify({ refresh: refreshToken }),
|
||
});
|
||
|
||
if (!response.ok) {
|
||
// Refresh token истёк или невалиден - нужно перелогиниться
|
||
this.clearToken();
|
||
throw new Error('Refresh token expired');
|
||
}
|
||
|
||
const data: LoginResponse = await response.json();
|
||
this.setToken(data.access);
|
||
|
||
// Если бэкенд вернул новый refresh token, сохраняем его
|
||
if (data.refresh) {
|
||
this.setRefreshToken(data.refresh);
|
||
}
|
||
|
||
return data.access;
|
||
}
|
||
|
||
private async request<T>(
|
||
endpoint: string,
|
||
options: RequestInit = {}
|
||
): Promise<T> {
|
||
const headers: Record<string, string> = {
|
||
...(options.headers as Record<string, string>),
|
||
};
|
||
|
||
// Устанавливаем Content-Type только если это не FormData
|
||
// Для FormData браузер сам установит правильный Content-Type с boundary
|
||
if (!(options.body instanceof FormData) && !headers['Content-Type']) {
|
||
headers['Content-Type'] = 'application/json';
|
||
}
|
||
|
||
// Автоматически добавляем токен если он есть
|
||
const token = this.getToken();
|
||
if (token) {
|
||
headers['Authorization'] = `Bearer ${token}`;
|
||
}
|
||
|
||
const response = await fetch(endpoint, {
|
||
...options,
|
||
headers,
|
||
});
|
||
|
||
// Если получили 401 и есть refresh token - пробуем обновить токен
|
||
if (response.status === 401 && this.getRefreshToken()) {
|
||
if (!this.isRefreshing) {
|
||
this.isRefreshing = true;
|
||
|
||
try {
|
||
const newToken = await this.refreshAccessToken();
|
||
this.isRefreshing = false;
|
||
this.onRefreshed(newToken);
|
||
|
||
// Повторяем оригинальный запрос с новым токеном
|
||
headers['Authorization'] = `Bearer ${newToken}`;
|
||
const retryResponse = await fetch(endpoint, {
|
||
...options,
|
||
headers,
|
||
});
|
||
|
||
if (!retryResponse.ok) {
|
||
let errorDetails = '';
|
||
try {
|
||
const errorData = await retryResponse.json();
|
||
errorDetails = errorData.detail || JSON.stringify(errorData);
|
||
} catch {
|
||
errorDetails = retryResponse.statusText;
|
||
}
|
||
throw new Error(`API Error: ${retryResponse.status} ${errorDetails}`);
|
||
}
|
||
|
||
// Обрабатываем успешный ответ
|
||
if (options.method === 'DELETE' && retryResponse.status === 204) {
|
||
return undefined as T;
|
||
}
|
||
|
||
const contentLength = retryResponse.headers.get('content-length');
|
||
if (contentLength === '0' || retryResponse.status === 204) {
|
||
return undefined as T;
|
||
}
|
||
|
||
return retryResponse.json();
|
||
} catch (refreshError) {
|
||
this.isRefreshing = false;
|
||
this.refreshSubscribers = [];
|
||
throw refreshError;
|
||
}
|
||
} else {
|
||
// Если уже идёт обновление токена, ждём его завершения
|
||
return new Promise((resolve, reject) => {
|
||
this.addRefreshSubscriber((newToken: string) => {
|
||
headers['Authorization'] = `Bearer ${newToken}`;
|
||
fetch(endpoint, { ...options, headers })
|
||
.then((res) => {
|
||
if (!res.ok) {
|
||
reject(new Error(`API Error: ${res.status}`));
|
||
return;
|
||
}
|
||
if (options.method === 'DELETE' && res.status === 204) {
|
||
resolve(undefined as T);
|
||
return;
|
||
}
|
||
return res.json();
|
||
})
|
||
.then((data) => resolve(data))
|
||
.catch(reject);
|
||
});
|
||
});
|
||
}
|
||
}
|
||
|
||
if (!response.ok) {
|
||
// Попытка получить детали ошибки
|
||
let errorDetails = '';
|
||
try {
|
||
const errorData = await response.json();
|
||
errorDetails = errorData.detail || JSON.stringify(errorData);
|
||
} catch {
|
||
errorDetails = response.statusText;
|
||
}
|
||
throw new Error(`API Error: ${response.status} ${errorDetails}`);
|
||
}
|
||
|
||
// Для DELETE запросов может не быть тела ответа
|
||
if (options.method === 'DELETE' && response.status === 204) {
|
||
return undefined as T;
|
||
}
|
||
|
||
// Проверяем, есть ли контент в ответе
|
||
const contentLength = response.headers.get('content-length');
|
||
if (contentLength === '0' || response.status === 204) {
|
||
return undefined as T;
|
||
}
|
||
|
||
return response.json();
|
||
}
|
||
|
||
// Авторизация
|
||
async login(credentials: LoginCredentials): Promise<void> {
|
||
const response = await fetch('/api/auth/token/', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
},
|
||
body: JSON.stringify(credentials),
|
||
});
|
||
|
||
if (!response.ok) {
|
||
throw new Error(`Login failed: ${response.status}`);
|
||
}
|
||
|
||
const data: LoginResponse = await response.json();
|
||
this.setToken(data.access);
|
||
this.setRefreshToken(data.refresh);
|
||
}
|
||
|
||
// Получение текущего пользователя
|
||
async getCurrentUser(): Promise<any> {
|
||
return this.request('/api/v1/employees/me/');
|
||
}
|
||
|
||
// Глобальный поиск (django-watson)
|
||
async search(query: string, limit?: number): Promise<any> {
|
||
const queryParams = new URLSearchParams();
|
||
queryParams.append('q', query);
|
||
if (limit) queryParams.append('limit', limit.toString());
|
||
|
||
const url = `/api/v1/search/?${queryParams.toString()}`;
|
||
return this.request(url);
|
||
}
|
||
|
||
// Сотрудники
|
||
async getEmployees(params?: { search?: string; department?: string; page?: number; limit?: number; is_active?: boolean }): Promise<any> {
|
||
const queryParams = new URLSearchParams();
|
||
if (params?.search) queryParams.append('search', params.search);
|
||
if (params?.department) queryParams.append('department', params.department);
|
||
if (params?.page) queryParams.append('page', params.page.toString());
|
||
if (params?.limit) queryParams.append('limit', params.limit.toString());
|
||
if (params?.is_active !== undefined) queryParams.append('active', params.is_active.toString());
|
||
|
||
const url = `/api/v1/employees/${queryParams.toString() ? '?' + queryParams.toString() : ''}`;
|
||
return this.request(url);
|
||
}
|
||
|
||
async getEmployee(id: number | string): Promise<any> {
|
||
return this.request(`/api/v1/employees/${id}/`);
|
||
}
|
||
|
||
async updateEmployee(id: number | string, data: {
|
||
first_name?: string;
|
||
last_name?: string;
|
||
patronymic?: string;
|
||
email?: string;
|
||
phone_number?: string;
|
||
telegram?: string;
|
||
whatsapp?: string;
|
||
wechat?: string;
|
||
birth_date?: string;
|
||
gender?: string;
|
||
position_id?: number;
|
||
}): Promise<any> {
|
||
return this.request(`/api/v1/employees/${id}/`, {
|
||
method: 'PATCH',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(data),
|
||
});
|
||
}
|
||
|
||
async uploadEmployeeAvatar(id: number | string, file: File): Promise<any> {
|
||
const formData = new FormData();
|
||
formData.append('avatar', file);
|
||
|
||
return this.request(`/api/v1/employees/${id}/`, {
|
||
method: 'PATCH',
|
||
body: formData,
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Создать кадровое событие (action) для сотрудника
|
||
*/
|
||
async createEmployeeAction(data: {
|
||
employee: number;
|
||
action: string;
|
||
date: string;
|
||
comment?: string;
|
||
}): Promise<any> {
|
||
return this.request('/api/v1/employee-actions/', {
|
||
method: 'POST',
|
||
body: JSON.stringify(data),
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Обновить кадровое событие
|
||
*/
|
||
async updateEmployeeAction(actionId: number, data: {
|
||
action?: string;
|
||
date?: string;
|
||
comment?: string;
|
||
}): Promise<any> {
|
||
return this.request(`/api/v1/employee-actions/${actionId}/`, {
|
||
method: 'PATCH',
|
||
body: JSON.stringify(data),
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Удалить кадровое событие
|
||
*/
|
||
async deleteEmployeeAction(actionId: number): Promise<any> {
|
||
return this.request(`/api/v1/employee-actions/${actionId}/`, {
|
||
method: 'DELETE',
|
||
});
|
||
}
|
||
|
||
// Отделы
|
||
async getDepartments(params?: { search?: string; page?: number; limit?: number }): Promise<any> {
|
||
const queryParams = new URLSearchParams();
|
||
if (params?.search) queryParams.append('search', params.search);
|
||
if (params?.page) queryParams.append('page', params.page.toString());
|
||
if (params?.limit) queryParams.append('limit', params.limit.toString());
|
||
|
||
const url = `/api/v1/departments/${queryParams.toString() ? '?' + queryParams.toString() : ''}`;
|
||
return this.request(url);
|
||
}
|
||
|
||
async getDepartment(id: number | string): Promise<any> {
|
||
return this.request(`/api/v1/departments/${id}/`);
|
||
}
|
||
|
||
// Посты (лента новостей)
|
||
async getPosts(params?: { page?: number; search?: string; limit?: number }): Promise<any> {
|
||
const queryParams = new URLSearchParams();
|
||
if (params?.page) queryParams.append('page', params.page.toString());
|
||
if (params?.search) queryParams.append('search', params.search);
|
||
if (params?.limit) queryParams.append('limit', params.limit.toString());
|
||
|
||
const url = `/api/v1/posts/${queryParams.toString() ? '?' + queryParams.toString() : ''}`;
|
||
return this.request(url);
|
||
}
|
||
|
||
// Документы
|
||
async getDocuments(params?: { search?: string; type?: string; status?: string; page?: number; limit?: number; folder_id?: number }): Promise<any> {
|
||
const queryParams = new URLSearchParams();
|
||
if (params?.search) queryParams.append('search', params.search);
|
||
if (params?.type) queryParams.append('type', params.type);
|
||
if (params?.status) queryParams.append('status', params.status);
|
||
if (params?.page) queryParams.append('page', params.page.toString());
|
||
if (params?.limit) queryParams.append('limit', params.limit.toString());
|
||
if (params?.folder_id !== undefined) queryParams.append('folder_id', params.folder_id.toString());
|
||
|
||
const url = `/api/v1/documents/${queryParams.toString() ? '?' + queryParams.toString() : ''}`;
|
||
return this.request(url);
|
||
}
|
||
|
||
async getDocument(id: number): Promise<any> {
|
||
return this.request(`/api/v1/documents/${id}/`);
|
||
}
|
||
|
||
async createDocument(data: {
|
||
title: string;
|
||
description?: string;
|
||
file: File | Blob;
|
||
extracted_text?: string;
|
||
sent_to_all?: boolean;
|
||
recipient_ids?: number[];
|
||
department_ids?: number[];
|
||
folder_id?: number | null;
|
||
acknowledgement_required?: boolean;
|
||
tag_ids?: number[];
|
||
}): Promise<any> {
|
||
const formData = new FormData();
|
||
formData.append('title', data.title);
|
||
if (data.description) formData.append('description', data.description);
|
||
if (data.extracted_text) formData.append('extracted_text', data.extracted_text);
|
||
if (data.folder_id) formData.append('folder', String(data.folder_id));
|
||
|
||
// Добавляем sent_to_all (по умолчанию true)
|
||
formData.append('sent_to_all', String(data.sent_to_all ?? true));
|
||
|
||
// Добавляем acknowledgement_required
|
||
if (data.acknowledgement_required !== undefined) {
|
||
formData.append('acknowledgement_required', String(data.acknowledgement_required));
|
||
}
|
||
|
||
// Добавляем получателей, если указаны
|
||
if (data.recipient_ids && data.recipient_ids.length > 0) {
|
||
data.recipient_ids.forEach(id => {
|
||
formData.append('recipient_ids', String(id));
|
||
});
|
||
}
|
||
|
||
// Добавляем отделы, если указаны
|
||
if (data.department_ids && data.department_ids.length > 0) {
|
||
data.department_ids.forEach(id => {
|
||
formData.append('department_ids', String(id));
|
||
});
|
||
}
|
||
|
||
// Добавляем теги, если указаны
|
||
if (data.tag_ids && data.tag_ids.length > 0) {
|
||
data.tag_ids.forEach(id => {
|
||
formData.append('tag_ids', String(id));
|
||
});
|
||
}
|
||
|
||
formData.append('file', data.file);
|
||
|
||
return this.request('/api/v1/documents/', {
|
||
method: 'POST',
|
||
body: formData,
|
||
});
|
||
}
|
||
|
||
async updateDocument(id: number, data: {
|
||
title?: string;
|
||
description?: string;
|
||
file?: File;
|
||
tag_ids?: number[];
|
||
folder?: number | null;
|
||
}): Promise<any> {
|
||
const formData = new FormData();
|
||
if (data.title !== undefined) formData.append('title', data.title);
|
||
if (data.description !== undefined) formData.append('description', data.description);
|
||
if (data.file) formData.append('file', data.file);
|
||
|
||
// Добавляем папку (может быть null)
|
||
if (data.folder !== undefined) {
|
||
if (data.folder === null) {
|
||
formData.append('folder', '');
|
||
} else {
|
||
formData.append('folder', String(data.folder));
|
||
}
|
||
}
|
||
|
||
// Добавляем теги
|
||
if (data.tag_ids !== undefined) {
|
||
data.tag_ids.forEach(tagId => {
|
||
formData.append('tag_ids', String(tagId));
|
||
});
|
||
}
|
||
|
||
return this.request(`/api/v1/documents/${id}/`, {
|
||
method: 'PATCH',
|
||
body: formData,
|
||
});
|
||
}
|
||
|
||
async deleteDocument(id: number): Promise<void> {
|
||
return this.request(`/api/v1/documents/${id}/`, {
|
||
method: 'DELETE',
|
||
});
|
||
}
|
||
|
||
// Document acknowledgement
|
||
async acknowledgeDocument(id: number): Promise<any> {
|
||
return this.request(`/api/v1/documents/${id}/acknowledge/`, {
|
||
method: 'POST',
|
||
body: JSON.stringify({}),
|
||
});
|
||
}
|
||
|
||
// Получить ведомость ознакомлений с документом (для начальника/автора)
|
||
async getDocumentAcknowledgements(id: number, search?: string): Promise<any> {
|
||
const queryParams = new URLSearchParams();
|
||
if (search) queryParams.append('search', search);
|
||
|
||
const url = `/api/v1/documents/${id}/acknowledgements/${queryParams.toString() ? '?' + queryParams.toString() : ''}`;
|
||
return this.request(url);
|
||
}
|
||
|
||
// Папки документов
|
||
async getFolders(params?: { parent_id?: number; root?: boolean }): Promise<any> {
|
||
const queryParams = new URLSearchParams();
|
||
if (params?.parent_id !== undefined) queryParams.append('parent_id', params.parent_id.toString());
|
||
if (params?.root) queryParams.append('root', 'true');
|
||
|
||
const url = `/api/v1/folders/${queryParams.toString() ? '?' + queryParams.toString() : ''}`;
|
||
return this.request(url);
|
||
}
|
||
|
||
async getFolder(id: number): Promise<any> {
|
||
return this.request(`/api/v1/folders/${id}/`);
|
||
}
|
||
|
||
async createFolder(data: { name: string; parent?: number | null }): Promise<any> {
|
||
return this.request('/api/v1/folders/', {
|
||
method: 'POST',
|
||
body: JSON.stringify(data),
|
||
});
|
||
}
|
||
|
||
async updateFolder(id: number, data: { name?: string; parent?: number | null }): Promise<any> {
|
||
return this.request(`/api/v1/folders/${id}/`, {
|
||
method: 'PATCH',
|
||
body: JSON.stringify(data),
|
||
});
|
||
}
|
||
|
||
async deleteFolder(id: number): Promise<void> {
|
||
return this.request(`/api/v1/folders/${id}/`, {
|
||
method: 'DELETE',
|
||
});
|
||
}
|
||
|
||
async getFolderChildren(id: number): Promise<any> {
|
||
return this.request(`/api/v1/folders/${id}/children/`);
|
||
}
|
||
|
||
async getFolderDocuments(id: number): Promise<any> {
|
||
return this.request(`/api/v1/folders/${id}/documents/`);
|
||
}
|
||
|
||
// Document Comments
|
||
async getDocumentComments(documentId: number): Promise<any> {
|
||
return this.request(`/api/v1/document-comments/?document=${documentId}`);
|
||
}
|
||
|
||
async createDocumentComment(data: { document: number; text: string; parent?: number }): Promise<any> {
|
||
return this.request('/api/v1/document-comments/', {
|
||
method: 'POST',
|
||
body: JSON.stringify({
|
||
document_id: data.document,
|
||
text: data.text,
|
||
parent_id: data.parent,
|
||
}),
|
||
});
|
||
}
|
||
|
||
async updateDocumentComment(id: number, text: string): Promise<any> {
|
||
return this.request(`/api/v1/document-comments/${id}/`, {
|
||
method: 'PATCH',
|
||
body: JSON.stringify({ text }),
|
||
});
|
||
}
|
||
|
||
async deleteDocumentComment(id: number): Promise<void> {
|
||
return this.request(`/api/v1/document-comments/${id}/`, {
|
||
method: 'DELETE',
|
||
});
|
||
}
|
||
|
||
async getCommentReplies(commentId: number): Promise<any> {
|
||
return this.request(`/api/v1/document-comments/${commentId}/replies/`);
|
||
}
|
||
|
||
// Document Tags
|
||
async getDocumentTags(): Promise<any> {
|
||
return this.request('/api/v1/document-tags/');
|
||
}
|
||
|
||
async getDocumentTag(id: number): Promise<any> {
|
||
return this.request(`/api/v1/document-tags/${id}/`);
|
||
}
|
||
|
||
async createDocumentTag(data: { name: string; color?: string }): Promise<any> {
|
||
return this.request('/api/v1/document-tags/', {
|
||
method: 'POST',
|
||
body: JSON.stringify(data),
|
||
});
|
||
}
|
||
|
||
async updateDocumentTag(id: number, data: { name?: string; color?: string }): Promise<any> {
|
||
return this.request(`/api/v1/document-tags/${id}/`, {
|
||
method: 'PATCH',
|
||
body: JSON.stringify(data),
|
||
});
|
||
}
|
||
|
||
async deleteDocumentTag(id: number): Promise<void> {
|
||
return this.request(`/api/v1/document-tags/${id}/`, {
|
||
method: 'DELETE',
|
||
});
|
||
}
|
||
|
||
async getDocumentsByTag(tagId: number): Promise<any> {
|
||
return this.request(`/api/v1/document-tags/${tagId}/documents/`);
|
||
}
|
||
|
||
// Related Documents
|
||
async getRelatedDocuments(id: number): Promise<any> {
|
||
return this.request(`/api/v1/documents/${id}/related/`);
|
||
}
|
||
|
||
async addRelatedDocument(id: number, relatedId: number): Promise<any> {
|
||
return this.request(`/api/v1/documents/${id}/add_related/`, {
|
||
method: 'POST',
|
||
body: JSON.stringify({ related_document_id: relatedId }),
|
||
});
|
||
}
|
||
|
||
async removeRelatedDocument(id: number, relatedId: number): Promise<any> {
|
||
return this.request(`/api/v1/documents/${id}/remove_related/`, {
|
||
method: 'DELETE',
|
||
body: JSON.stringify({ related_document_id: relatedId }),
|
||
});
|
||
}
|
||
|
||
// Document Thumbnails
|
||
getDocumentThumbnail(id: number, size?: 'small' | 'medium' | 'large' | 'original'): string {
|
||
const sizeParam = size ? `?size=${size}` : '';
|
||
return `/api/v1/documents/${id}/thumbnail/${sizeParam}`;
|
||
}
|
||
|
||
// Заявки
|
||
async getRequests(params?: {
|
||
status?: string;
|
||
type?: string;
|
||
search?: string;
|
||
page?: number;
|
||
limit?: number;
|
||
view?: string;
|
||
addressed_to_me?: string;
|
||
employee_id?: string | number;
|
||
created_from?: string;
|
||
created_to?: string;
|
||
date_from?: string;
|
||
date_to?: string;
|
||
[key: string]: any;
|
||
}): Promise<any> {
|
||
const queryParams = new URLSearchParams();
|
||
if (params?.status) queryParams.append('status', params.status);
|
||
if (params?.type) queryParams.append('type', params.type);
|
||
if (params?.search) queryParams.append('search', params.search);
|
||
if (params?.page) queryParams.append('page', params.page.toString());
|
||
if (params?.limit) queryParams.append('limit', params.limit.toString());
|
||
if (params?.view) queryParams.append('view', params.view);
|
||
if (params?.addressed_to_me) queryParams.append('addressed_to_me', params.addressed_to_me);
|
||
if (params?.employee_id) queryParams.append('employee_id', params.employee_id.toString());
|
||
if (params?.created_from) queryParams.append('created_from', params.created_from);
|
||
if (params?.created_to) queryParams.append('created_to', params.created_to);
|
||
if (params?.date_from) queryParams.append('date_from', params.date_from);
|
||
if (params?.date_to) queryParams.append('date_to', params.date_to);
|
||
|
||
const url = `/api/v1/requests/${queryParams.toString() ? '?' + queryParams.toString() : ''}`;
|
||
return this.request(url);
|
||
}
|
||
|
||
async createRequest(data: any, saveAs?: 'draft' | 'submitted'): Promise<any> {
|
||
const formData = new FormData();
|
||
|
||
Object.keys(data).forEach((key) => {
|
||
if (key === 'attachments' && Array.isArray(data[key])) {
|
||
data[key].forEach((file: File) => {
|
||
formData.append('attachments', file);
|
||
});
|
||
} else if (key === 'attachment' && data[key] instanceof File) {
|
||
formData.append('attachment', data[key]);
|
||
} else if (Array.isArray(data[key])) {
|
||
data[key].forEach((val: any) => {
|
||
formData.append(key, String(val));
|
||
});
|
||
} else if (data[key] !== null && data[key] !== undefined) {
|
||
formData.append(key, String(data[key]));
|
||
}
|
||
});
|
||
|
||
if (saveAs) {
|
||
formData.append('save_as', saveAs);
|
||
}
|
||
|
||
const token = this.getToken();
|
||
const headers: Record<string, string> = {};
|
||
if (token) {
|
||
headers['Authorization'] = `Bearer ${token}`;
|
||
}
|
||
|
||
const response = await fetch('/api/v1/requests/', {
|
||
method: 'POST',
|
||
headers,
|
||
body: formData,
|
||
});
|
||
|
||
if (!response.ok) {
|
||
let detail = `${response.status} ${response.statusText}`;
|
||
try {
|
||
const body = await response.json();
|
||
detail = JSON.stringify(body);
|
||
} catch {}
|
||
throw new Error(detail);
|
||
}
|
||
|
||
return response.json();
|
||
}
|
||
|
||
async updateRequest(requestId: number, data: any, saveAs?: 'draft' | 'submitted'): Promise<any> {
|
||
const formData = new FormData();
|
||
|
||
Object.keys(data).forEach((key) => {
|
||
if (key === 'attachments' && Array.isArray(data[key])) {
|
||
data[key].forEach((file: File) => {
|
||
formData.append('attachments', file);
|
||
});
|
||
} else if (key === 'attachment' && data[key] instanceof File) {
|
||
formData.append('attachment', data[key]);
|
||
} else if (Array.isArray(data[key])) {
|
||
data[key].forEach((val: any) => {
|
||
formData.append(key, String(val));
|
||
});
|
||
} else if (data[key] !== null && data[key] !== undefined) {
|
||
formData.append(key, String(data[key]));
|
||
}
|
||
});
|
||
|
||
if (saveAs) {
|
||
formData.append('save_as', saveAs);
|
||
}
|
||
|
||
const token = this.getToken();
|
||
const headers: Record<string, string> = {};
|
||
if (token) {
|
||
headers['Authorization'] = `Bearer ${token}`;
|
||
}
|
||
|
||
const response = await fetch(`/api/v1/requests/${requestId}/`, {
|
||
method: 'PATCH',
|
||
headers,
|
||
body: formData,
|
||
});
|
||
|
||
if (!response.ok) {
|
||
let detail = `${response.status} ${response.statusText}`;
|
||
try {
|
||
const body = await response.json();
|
||
detail = JSON.stringify(body);
|
||
} catch {}
|
||
throw new Error(detail);
|
||
}
|
||
|
||
return response.json();
|
||
}
|
||
|
||
async deleteRequest(requestId: number): Promise<void> {
|
||
await this.request(`/api/v1/requests/${requestId}/`, {
|
||
method: 'DELETE',
|
||
});
|
||
}
|
||
|
||
async getRequestComments(requestId: number): Promise<any> {
|
||
return this.request(`/api/v1/requests/${requestId}/comments/`);
|
||
}
|
||
|
||
async deleteRequestComment(requestId: number, commentId: number): Promise<void> {
|
||
await this.request(`/api/v1/requests/${requestId}/comments/${commentId}/`, {
|
||
method: 'DELETE',
|
||
});
|
||
}
|
||
|
||
// Чаты (основные методы чатов ниже в файле)
|
||
async getChatMessagesAround(chatId: number | string, params?: { limit?: number; around_id?: number }): Promise<any> {
|
||
const queryParams = new URLSearchParams();
|
||
if (params?.limit) queryParams.append('limit', params.limit.toString());
|
||
if (params?.around_id) queryParams.append('around_id', params.around_id.toString());
|
||
|
||
const url = `/api/v1/communications/chats/${chatId}/messages-around/${queryParams.toString() ? '?' + queryParams.toString() : ''}`;
|
||
return this.request(url);
|
||
}
|
||
|
||
async sendMessage(chatId: number | string, text: string, replyTo?: number): Promise<any> {
|
||
const formData = new FormData();
|
||
formData.append('content', text);
|
||
formData.append('chat_id', chatId.toString());
|
||
if (replyTo) {
|
||
formData.append('reply_to', replyTo.toString());
|
||
}
|
||
|
||
const token = this.getToken();
|
||
const headers: Record<string, string> = {};
|
||
if (token) {
|
||
headers['Authorization'] = `Bearer ${token}`;
|
||
}
|
||
|
||
const response = await fetch(`/api/v1/communications/messages/upload/`, {
|
||
method: 'POST',
|
||
headers,
|
||
body: formData,
|
||
});
|
||
|
||
if (!response.ok) {
|
||
let errorDetails = '';
|
||
try {
|
||
const errorData = await response.json();
|
||
errorDetails = errorData.detail || JSON.stringify(errorData);
|
||
} catch {
|
||
errorDetails = response.statusText;
|
||
}
|
||
throw new Error(`API Error: ${response.status} ${errorDetails}`);
|
||
}
|
||
|
||
const result = await response.json();
|
||
return result.message || result;
|
||
}
|
||
|
||
async sendMessageWithFiles(chatId: number | string, text: string, files: File[], replyTo?: number): Promise<any> {
|
||
const formData = new FormData();
|
||
formData.append('content', text);
|
||
formData.append('chat_id', chatId.toString());
|
||
if (replyTo) {
|
||
formData.append('reply_to', replyTo.toString());
|
||
}
|
||
files.forEach((file, index) => {
|
||
formData.append(`file_${index}`, file);
|
||
});
|
||
|
||
const token = this.getToken();
|
||
const headers: Record<string, string> = {};
|
||
if (token) {
|
||
headers['Authorization'] = `Bearer ${token}`;
|
||
}
|
||
|
||
const response = await fetch(`/api/v1/communications/messages/upload/`, {
|
||
method: 'POST',
|
||
headers,
|
||
body: formData,
|
||
});
|
||
|
||
if (!response.ok) {
|
||
let errorDetails = '';
|
||
try {
|
||
const errorData = await response.json();
|
||
errorDetails = errorData.detail || JSON.stringify(errorData);
|
||
} catch {
|
||
errorDetails = response.statusText;
|
||
}
|
||
throw new Error(`API Error: ${response.status} ${errorDetails}`);
|
||
}
|
||
|
||
const result = await response.json();
|
||
return result.message || result;
|
||
}
|
||
|
||
async updateMessage(messageId: number, text: string): Promise<any> {
|
||
return this.request(`/api/v1/communications/messages/${messageId}/`, {
|
||
method: 'PATCH',
|
||
body: JSON.stringify({ content: text }),
|
||
});
|
||
}
|
||
|
||
async deleteMessage(messageId: number): Promise<void> {
|
||
await this.request(`/api/v1/communications/messages/${messageId}/`, {
|
||
method: 'DELETE',
|
||
});
|
||
}
|
||
|
||
// Уведомления
|
||
async getNotifications(params?: { page?: number; page_size?: number; unread_only?: boolean }): Promise<any> {
|
||
const queryParams = new URLSearchParams();
|
||
if (params?.page) queryParams.append('page', String(params.page));
|
||
if (params?.page_size) queryParams.append('page_size', String(params.page_size));
|
||
if (params?.unread_only) queryParams.append('unread_only', 'true');
|
||
|
||
const url = queryParams.toString()
|
||
? `/api/v1/notifications/?${queryParams.toString()}`
|
||
: '/api/v1/notifications/';
|
||
return this.request(url);
|
||
}
|
||
|
||
async getUnreadNotificationsCount(): Promise<{ count: number }> {
|
||
return this.request('/api/v1/notifications/count/');
|
||
}
|
||
|
||
async markNotificationAsRead(id: number): Promise<void> {
|
||
await this.request(`/api/v1/notifications/${id}/read/`, {
|
||
method: 'POST',
|
||
});
|
||
}
|
||
|
||
async markAllNotificationsAsRead(): Promise<void> {
|
||
await this.request('/api/v1/notifications/read-all/', {
|
||
method: 'POST',
|
||
});
|
||
}
|
||
|
||
async markCategoryAsRead(category: string): Promise<{ status: string; count: number }> {
|
||
// Получаем список verb'ов для категории
|
||
const { getVerbsByCategory } = await import('./verbTranslations');
|
||
const verbs = getVerbsByCategory(category);
|
||
|
||
return this.request('/api/v1/notifications/category/read/', {
|
||
method: 'POST',
|
||
body: JSON.stringify({ verbs, category }),
|
||
});
|
||
}
|
||
|
||
// Web Push подписки
|
||
async getVapidPublicKey(): Promise<{ vapid_public_key: string }> {
|
||
return this.request('/api/v1/notifications/push/vapid-key/');
|
||
}
|
||
|
||
async subscribePush(data: {
|
||
endpoint: string;
|
||
keys: { p256dh: string; auth: string };
|
||
device_name?: string;
|
||
}): Promise<{ status: string; message: string; created: boolean }> {
|
||
return this.request('/api/v1/notifications/push/subscribe/', {
|
||
method: 'POST',
|
||
body: JSON.stringify(data),
|
||
});
|
||
}
|
||
|
||
async unsubscribePush(endpoint?: string): Promise<{ status: string; message: string }> {
|
||
return this.request('/api/v1/notifications/push/unsubscribe/', {
|
||
method: 'DELETE',
|
||
body: JSON.stringify({ endpoint }),
|
||
});
|
||
}
|
||
|
||
async getPushSubscriptions(): Promise<{ subscriptions: any[] }> {
|
||
return this.request('/api/v1/notifications/push/subscriptions/');
|
||
}
|
||
|
||
// Дополнительные методы уведомлений
|
||
async deleteNotification(id: number): Promise<void> {
|
||
await this.request(`/api/v1/notifications/${id}/`, {
|
||
method: 'DELETE',
|
||
});
|
||
}
|
||
|
||
async deleteAllReadNotifications(): Promise<{ status: string; count: number }> {
|
||
return this.request('/api/v1/notifications/delete-all-read/', {
|
||
method: 'DELETE',
|
||
});
|
||
}
|
||
|
||
async getNotificationPreferences(): Promise<any> {
|
||
return this.request('/api/v1/notifications/preferences/');
|
||
}
|
||
|
||
async updateNotificationPreferences(data: {
|
||
web_enabled?: boolean;
|
||
email_enabled?: boolean;
|
||
email_frequency?: 'instant' | 'daily' | 'weekly' | 'disabled';
|
||
push_enabled?: boolean;
|
||
dnd_enabled?: boolean;
|
||
dnd_start_time?: string;
|
||
dnd_end_time?: string;
|
||
disabled_verbs?: string[];
|
||
}): Promise<any> {
|
||
return this.request('/api/v1/notifications/preferences/', {
|
||
method: 'PUT',
|
||
body: JSON.stringify(data),
|
||
});
|
||
}
|
||
|
||
async getVerbTypes(): Promise<{ verb_types: any[] }> {
|
||
return this.request('/api/v1/notifications/verb-types/');
|
||
}
|
||
|
||
// Календарь (django-scheduler API)
|
||
async getCalendarEvents(params?: { start?: string; end?: string; calendar?: number }): Promise<any> {
|
||
const queryParams = new URLSearchParams();
|
||
if (params?.start) queryParams.append('start', params.start);
|
||
if (params?.end) queryParams.append('end', params.end);
|
||
if (params?.calendar) queryParams.append('calendar', params.calendar.toString());
|
||
|
||
const url = `/api/v1/schedule/events/${queryParams.toString() ? '?' + queryParams.toString() : ''}`;
|
||
return this.request(url);
|
||
}
|
||
|
||
async getMyEvents(params?: { start?: string; end?: string }): Promise<any> {
|
||
const queryParams = new URLSearchParams();
|
||
if (params?.start) queryParams.append('start', params.start);
|
||
if (params?.end) queryParams.append('end', params.end);
|
||
|
||
const url = `/api/v1/schedule/events/my-events/${queryParams.toString() ? '?' + queryParams.toString() : ''}`;
|
||
return this.request(url);
|
||
}
|
||
|
||
async getCalendars(): Promise<any> {
|
||
return this.request('/api/v1/schedule/calendars/');
|
||
}
|
||
|
||
async getCalendarSubscriptions(): Promise<any> {
|
||
return this.request('/api/v1/schedule/subscriptions/');
|
||
}
|
||
|
||
async subscribeToCalendar(calendarId: number): Promise<any> {
|
||
return this.request('/api/v1/schedule/subscriptions/', {
|
||
method: 'POST',
|
||
body: JSON.stringify({ calendar: calendarId }),
|
||
});
|
||
}
|
||
|
||
async unsubscribeFromCalendar(subscriptionId: number): Promise<any> {
|
||
return this.request(`/api/v1/schedule/subscriptions/${subscriptionId}/`, {
|
||
method: 'DELETE',
|
||
});
|
||
}
|
||
|
||
async updateSubscription(subscriptionId: number, data: any): Promise<any> {
|
||
return this.request(`/api/v1/schedule/subscriptions/${subscriptionId}/`, {
|
||
method: 'PATCH',
|
||
body: JSON.stringify(data),
|
||
});
|
||
}
|
||
|
||
async getCalendar(id: number): Promise<any> {
|
||
return this.request(`/api/v1/schedule/calendars/${id}/`);
|
||
}
|
||
|
||
async createCalendar(data: { name: string; slug?: string }): Promise<any> {
|
||
return this.request('/api/v1/schedule/calendars/', {
|
||
method: 'POST',
|
||
body: JSON.stringify(data),
|
||
});
|
||
}
|
||
|
||
async updateCalendar(id: number, data: { name?: string; slug?: string }): Promise<any> {
|
||
return this.request(`/api/v1/schedule/calendars/${id}/`, {
|
||
method: 'PATCH',
|
||
body: JSON.stringify(data),
|
||
});
|
||
}
|
||
|
||
async deleteCalendar(id: number): Promise<void> {
|
||
await this.request(`/api/v1/schedule/calendars/${id}/`, {
|
||
method: 'DELETE',
|
||
});
|
||
}
|
||
|
||
async getEvent(id: number): Promise<any> {
|
||
return this.request(`/api/v1/schedule/events/${id}/`);
|
||
}
|
||
|
||
async createEvent(data: {
|
||
title: string;
|
||
description?: string;
|
||
start: string;
|
||
end: string;
|
||
calendar: number;
|
||
color_event?: string;
|
||
rule?: number | null;
|
||
}): Promise<any> {
|
||
return this.request('/api/v1/schedule/events/', {
|
||
method: 'POST',
|
||
body: JSON.stringify(data),
|
||
});
|
||
}
|
||
|
||
async updateEvent(id: number, data: {
|
||
title?: string;
|
||
description?: string;
|
||
start?: string;
|
||
end?: string;
|
||
calendar?: number;
|
||
color_event?: string;
|
||
rule?: number | null;
|
||
}): Promise<any> {
|
||
return this.request(`/api/v1/schedule/events/${id}/`, {
|
||
method: 'PATCH',
|
||
body: JSON.stringify(data),
|
||
});
|
||
}
|
||
|
||
async deleteEvent(id: number): Promise<void> {
|
||
await this.request(`/api/v1/schedule/events/${id}/`, {
|
||
method: 'DELETE',
|
||
});
|
||
}
|
||
|
||
// Calendar Participants (CalendarRelation)
|
||
async getCalendarParticipants(calendarId: number): Promise<any> {
|
||
return this.request(`/api/v1/schedule/calendars/${calendarId}/participants/`);
|
||
}
|
||
|
||
async addCalendarParticipant(calendarId: number, userId: number, distinction: string = 'viewer'): Promise<any> {
|
||
return this.request(`/api/v1/schedule/calendars/${calendarId}/add-participant/`, {
|
||
method: 'POST',
|
||
body: JSON.stringify({ user_id: userId, distinction }),
|
||
});
|
||
}
|
||
|
||
async removeCalendarParticipant(calendarId: number, userId: number): Promise<void> {
|
||
await this.request(`/api/v1/schedule/calendars/${calendarId}/remove-participant/${userId}/`, {
|
||
method: 'DELETE',
|
||
});
|
||
}
|
||
|
||
// Calendar Export
|
||
async exportCalendarToICS(calendarId: number): Promise<Blob> {
|
||
const response = await fetch(`/api/v1/schedule/calendars/${calendarId}/export-ical/`, {
|
||
method: 'GET',
|
||
headers: {
|
||
'Authorization': `Bearer ${this.getToken()}`,
|
||
},
|
||
});
|
||
|
||
if (!response.ok) {
|
||
throw new Error('Failed to export calendar');
|
||
}
|
||
|
||
return response.blob();
|
||
}
|
||
|
||
// Calendar Import
|
||
async importCalendarFromICS(calendarId: number, file: File): Promise<any> {
|
||
const formData = new FormData();
|
||
formData.append('file', file);
|
||
|
||
const response = await fetch(`/api/v1/schedule/calendars/${calendarId}/import-ical/`, {
|
||
method: 'POST',
|
||
headers: {
|
||
'Authorization': `Bearer ${this.getToken()}`,
|
||
},
|
||
body: formData,
|
||
});
|
||
|
||
if (!response.ok) {
|
||
const error = await response.json();
|
||
throw new Error(error.error || 'Failed to import calendar');
|
||
}
|
||
|
||
return response.json();
|
||
}
|
||
|
||
// Recurring Events (Rules)
|
||
async createRule(data: {
|
||
name: string;
|
||
description?: string;
|
||
frequency: string;
|
||
params?: any;
|
||
}): Promise<any> {
|
||
// Сериализуем params в JSON-строку для django-scheduler
|
||
const payload = {
|
||
...data,
|
||
params: data.params ? JSON.stringify(data.params) : '{}',
|
||
};
|
||
|
||
return this.request('/api/v1/schedule/rules/', {
|
||
method: 'POST',
|
||
body: JSON.stringify(payload),
|
||
});
|
||
}
|
||
|
||
async updateRule(ruleId: number, data: {
|
||
name: string;
|
||
description?: string;
|
||
frequency: string;
|
||
params?: any;
|
||
}): Promise<any> {
|
||
// Сериализуем params в JSON-строку для django-scheduler
|
||
const payload = {
|
||
...data,
|
||
params: data.params ? JSON.stringify(data.params) : '{}',
|
||
};
|
||
|
||
return this.request(`/api/v1/schedule/rules/${ruleId}/`, {
|
||
method: 'PUT',
|
||
body: JSON.stringify(payload),
|
||
});
|
||
}
|
||
|
||
async getRules(): Promise<any> {
|
||
return this.request('/api/v1/schedule/rules/');
|
||
}
|
||
|
||
async getRule(ruleId: number): Promise<any> {
|
||
return this.request(`/api/v1/schedule/rules/${ruleId}/`);
|
||
}
|
||
|
||
async getOccurrences(params: { start: string; end: string; calendar?: number }): Promise<any> {
|
||
const queryParams = new URLSearchParams();
|
||
queryParams.append('start', params.start);
|
||
queryParams.append('end', params.end);
|
||
if (params.calendar) queryParams.append('calendar', params.calendar.toString());
|
||
|
||
return this.request(`/api/v1/schedule/events/occurrences/?${queryParams.toString()}`);
|
||
}
|
||
|
||
// Event Participants (EventRelation)
|
||
async getEventParticipants(eventId: number): Promise<any> {
|
||
return this.request(`/api/v1/schedule/relations/?event=${eventId}`);
|
||
}
|
||
|
||
async addEventParticipant(eventId: number, userId: number, distinction: string = 'attendee'): Promise<any> {
|
||
return this.request('/api/v1/schedule/relations/', {
|
||
method: 'POST',
|
||
body: JSON.stringify({
|
||
event: eventId,
|
||
object_id: userId,
|
||
distinction,
|
||
}),
|
||
});
|
||
}
|
||
|
||
async removeEventParticipant(relationId: number): Promise<any> {
|
||
return this.request(`/api/v1/schedule/relations/${relationId}/`, {
|
||
method: 'DELETE',
|
||
});
|
||
}
|
||
|
||
// Посты - CRUD операции
|
||
async createPost(data: {
|
||
type?: 'company' | 'department' | 'employee';
|
||
title?: string;
|
||
body?: string;
|
||
content?: string;
|
||
department?: number;
|
||
image?: File;
|
||
attachment?: File;
|
||
attachments?: File[];
|
||
}): Promise<any> {
|
||
const formData = new FormData();
|
||
|
||
// API требует title и body как отдельные поля
|
||
if (data.title) formData.append('title', data.title);
|
||
if (data.body) formData.append('body', data.body);
|
||
if (data.content) formData.append('content', data.content);
|
||
|
||
// Опциональные поля
|
||
if (data.type) formData.append('type', data.type);
|
||
if (data.department) formData.append('department', String(data.department));
|
||
if (data.image) formData.append('image', data.image);
|
||
if (data.attachment) formData.append('attachment', data.attachment);
|
||
|
||
// Вложения (attachments)
|
||
if (data.attachments) {
|
||
data.attachments.forEach((file: File) => {
|
||
formData.append('attachments', file);
|
||
});
|
||
}
|
||
|
||
const token = this.getToken();
|
||
const headers: HeadersInit = {};
|
||
if (token) {
|
||
headers['Authorization'] = `Bearer ${token}`;
|
||
}
|
||
|
||
const response = await fetch('/api/v1/posts/', {
|
||
method: 'POST',
|
||
headers,
|
||
body: formData,
|
||
});
|
||
|
||
if (!response.ok) {
|
||
const errorText = await response.text();
|
||
console.error('API Error Response:', errorText);
|
||
throw new Error(`API Error: ${response.status} ${response.statusText}`);
|
||
}
|
||
|
||
return response.json();
|
||
}
|
||
|
||
async updatePost(postId: number, data: {
|
||
type?: 'company' | 'department' | 'employee';
|
||
title?: string;
|
||
body?: string;
|
||
content?: string;
|
||
department?: number;
|
||
image?: File;
|
||
attachment?: File;
|
||
attachments?: File[];
|
||
}): Promise<any> {
|
||
const formData = new FormData();
|
||
|
||
// API требует title и body как отдельные поля
|
||
if (data.title) formData.append('title', data.title);
|
||
if (data.body) formData.append('body', data.body);
|
||
if (data.content) formData.append('content', data.content);
|
||
|
||
// Опциональные поля
|
||
if (data.type) formData.append('type', data.type);
|
||
if (data.department !== undefined) formData.append('department', String(data.department));
|
||
if (data.image) formData.append('image', data.image);
|
||
if (data.attachment) formData.append('attachment', data.attachment);
|
||
|
||
// Вложения (attachments)
|
||
if (data.attachments) {
|
||
data.attachments.forEach((file: File) => {
|
||
formData.append('attachments', file);
|
||
});
|
||
}
|
||
|
||
const token = this.getToken();
|
||
const headers: HeadersInit = {};
|
||
if (token) {
|
||
headers['Authorization'] = `Bearer ${token}`;
|
||
}
|
||
|
||
const response = await fetch(`/api/v1/posts/${postId}/`, {
|
||
method: 'PATCH',
|
||
headers,
|
||
body: formData,
|
||
});
|
||
|
||
if (!response.ok) {
|
||
const errorText = await response.text();
|
||
console.error('API Error Response:', errorText);
|
||
throw new Error(`API Error: ${response.status} ${response.statusText}`);
|
||
}
|
||
|
||
return response.json();
|
||
}
|
||
|
||
async deletePost(postId: number): Promise<void> {
|
||
await this.request(`/api/v1/posts/${postId}/`, {
|
||
method: 'DELETE',
|
||
});
|
||
}
|
||
|
||
// Лайки
|
||
async likePost(postId: number): Promise<any> {
|
||
return this.request(`/api/v1/posts/${postId}/like/`, {
|
||
method: 'POST',
|
||
});
|
||
}
|
||
|
||
async unlikePost(postId: number): Promise<any> {
|
||
return this.request(`/api/v1/posts/${postId}/unlike/`, {
|
||
method: 'POST',
|
||
});
|
||
}
|
||
|
||
async getPostLikers(postId: number): Promise<any> {
|
||
return this.request(`/api/v1/posts/${postId}/likers/`);
|
||
}
|
||
|
||
// Комментарии
|
||
async getComments(params: { post: number; page?: number }): Promise<any> {
|
||
const queryParams = new URLSearchParams();
|
||
if (params.page) queryParams.append('page', params.page.toString());
|
||
|
||
const url = `/api/v1/posts/${params.post}/comments/${queryParams.toString() ? '?' + queryParams.toString() : ''}`;
|
||
return this.request(url);
|
||
}
|
||
|
||
async createComment(postId: number, text: string, image?: File, attachment?: File): Promise<any> {
|
||
if (image || attachment) {
|
||
const formData = new FormData();
|
||
formData.append('text', text);
|
||
if (image) formData.append('image', image);
|
||
if (attachment) formData.append('attachment', attachment);
|
||
|
||
const token = this.getToken();
|
||
const headers: HeadersInit = {};
|
||
if (token) {
|
||
headers['Authorization'] = `Bearer ${token}`;
|
||
}
|
||
|
||
const response = await fetch(`/api/v1/posts/${postId}/comments/`, {
|
||
method: 'POST',
|
||
headers,
|
||
body: formData,
|
||
});
|
||
|
||
if (!response.ok) {
|
||
const errorText = await response.text();
|
||
console.error('API Error Response:', errorText);
|
||
throw new Error(`API Error: ${response.status} ${response.statusText}`);
|
||
}
|
||
|
||
return response.json();
|
||
}
|
||
|
||
return this.request(`/api/v1/posts/${postId}/comments/`, {
|
||
method: 'POST',
|
||
body: JSON.stringify({ text }),
|
||
});
|
||
}
|
||
|
||
async updateComment(commentId: number, text: string): Promise<any> {
|
||
const response = await this.request<any>(`/api/v1/communications/messages/${commentId}/`, {
|
||
method: 'PATCH',
|
||
body: JSON.stringify({ content: text }),
|
||
});
|
||
|
||
// Конвертируем content -> text для совместимости
|
||
return {
|
||
...response,
|
||
text: response.content || text,
|
||
};
|
||
}
|
||
|
||
async deleteComment(commentId: number): Promise<void> {
|
||
await this.request(`/api/v1/communications/messages/${commentId}/`, {
|
||
method: 'DELETE',
|
||
});
|
||
}
|
||
|
||
// Управление заявками (approve/reject/cancel)
|
||
async approveRequest(requestId: number): Promise<any> {
|
||
return this.request(`/api/v1/requests/${requestId}/approve/`, {
|
||
method: 'POST',
|
||
});
|
||
}
|
||
|
||
async rejectRequest(requestId: number): Promise<any> {
|
||
return this.request(`/api/v1/requests/${requestId}/reject/`, {
|
||
method: 'POST',
|
||
});
|
||
}
|
||
|
||
async cancelRequest(requestId: number): Promise<any> {
|
||
return this.request(`/api/v1/requests/${requestId}/cancel/`, {
|
||
method: 'POST',
|
||
});
|
||
}
|
||
|
||
async addRequestComment(requestId: number, text: string): Promise<any> {
|
||
return this.request(`/api/v1/requests/${requestId}/comments/`, {
|
||
method: 'POST',
|
||
body: JSON.stringify({ text }),
|
||
});
|
||
}
|
||
|
||
// Профиль пользователя
|
||
async updateCurrentUserProfile(data: any): Promise<any> {
|
||
const formData = new FormData();
|
||
|
||
Object.keys(data).forEach((key) => {
|
||
if (data[key] instanceof File) {
|
||
formData.append(key, data[key]);
|
||
} else if (data[key] !== null && data[key] !== undefined) {
|
||
formData.append(key, data[key]);
|
||
}
|
||
});
|
||
|
||
const token = this.getToken();
|
||
const headers: Record<string, string> = {};
|
||
if (token) {
|
||
headers['Authorization'] = `Bearer ${token}`;
|
||
}
|
||
|
||
const response = await fetch('/api/v1/employees/me/', {
|
||
method: 'PATCH',
|
||
headers,
|
||
body: formData,
|
||
});
|
||
|
||
if (!response.ok) {
|
||
throw new Error(`API Error: ${response.status} ${response.statusText}`);
|
||
}
|
||
|
||
return response.json();
|
||
}
|
||
|
||
// Реакции на сообщения
|
||
async reactToMessage(messageId: number, emoji: string): Promise<any> {
|
||
return this.request(`/api/v1/communications/messages/${messageId}/react/`, {
|
||
method: 'POST',
|
||
body: JSON.stringify({ emoji }),
|
||
});
|
||
}
|
||
|
||
async unreactToMessage(messageId: number, emoji: string): Promise<any> {
|
||
return this.request(`/api/v1/communications/messages/${messageId}/unreact/`, {
|
||
method: 'POST',
|
||
body: JSON.stringify({ emoji }),
|
||
});
|
||
}
|
||
|
||
// ============= Communications (Chats & Messages) API =============
|
||
|
||
/**
|
||
* Получить список чатов пользователя
|
||
*/
|
||
async getChats(params?: { search?: string; page?: number }): Promise<any> {
|
||
const queryParams = new URLSearchParams();
|
||
if (params?.search) queryParams.append('search', params.search);
|
||
if (params?.page) queryParams.append('page', params.page.toString());
|
||
|
||
const query = queryParams.toString();
|
||
return this.request(`/api/v1/communications/chats/${query ? '?' + query : ''}`);
|
||
}
|
||
|
||
/**
|
||
* Получить детали чата по ID
|
||
*/
|
||
async getChat(chatId: number): Promise<any> {
|
||
return this.request(`/api/v1/communications/chats/${chatId}/`);
|
||
}
|
||
|
||
/**
|
||
* Создать новый чат
|
||
* type: 'private' | 'group' | 'department' | 'general'
|
||
*/
|
||
async createChat(data: {
|
||
type: string;
|
||
participants?: number[];
|
||
name?: string;
|
||
description?: string;
|
||
department?: number;
|
||
include_all_employees?: boolean;
|
||
avatar?: File;
|
||
}): Promise<any> {
|
||
// Если есть файл, используем FormData
|
||
if (data.avatar) {
|
||
const formData = new FormData();
|
||
formData.append('type', data.type);
|
||
if (data.name) formData.append('name', data.name);
|
||
if (data.description) formData.append('description', data.description);
|
||
if (data.avatar) formData.append('avatar', data.avatar);
|
||
if (data.participants) {
|
||
data.participants.forEach(id => formData.append('participants', id.toString()));
|
||
}
|
||
if (data.department) formData.append('department', data.department.toString());
|
||
if (data.include_all_employees !== undefined) {
|
||
formData.append('include_all_employees', data.include_all_employees.toString());
|
||
}
|
||
|
||
return this.request('/api/v1/communications/chats/', {
|
||
method: 'POST',
|
||
body: formData,
|
||
});
|
||
}
|
||
|
||
// Без файла - обычный JSON
|
||
return this.request('/api/v1/communications/chats/', {
|
||
method: 'POST',
|
||
body: JSON.stringify(data),
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Получить сообщения чата с пагинацией
|
||
*/
|
||
async getChatMessages(
|
||
chatId: number,
|
||
params?: {
|
||
limit?: number;
|
||
before_id?: number;
|
||
after_id?: number;
|
||
mark_read?: boolean;
|
||
before?: string;
|
||
after?: string;
|
||
}
|
||
): Promise<any> {
|
||
const queryParams = new URLSearchParams();
|
||
if (params?.limit) queryParams.append('limit', params.limit.toString());
|
||
if (params?.before_id) queryParams.append('before_id', params.before_id.toString());
|
||
if (params?.after_id) queryParams.append('after_id', params.after_id.toString());
|
||
if (typeof params?.mark_read === 'boolean') queryParams.append('mark_read', String(params.mark_read));
|
||
if (params?.before) queryParams.append('before', params.before);
|
||
if (params?.after) queryParams.append('after', params.after);
|
||
|
||
const query = queryParams.toString();
|
||
return this.request(`/api/v1/communications/chats/${chatId}/messages/${query ? '?' + query : ''}`);
|
||
}
|
||
|
||
/**
|
||
* Пометить чат как прочитанный
|
||
*/
|
||
async markChatAsRead(chatId: number, lastReadMessageId?: number): Promise<any> {
|
||
return this.request(`/api/v1/communications/chats/${chatId}/mark-read/`, {
|
||
method: 'POST',
|
||
body: JSON.stringify({
|
||
message_id: lastReadMessageId
|
||
}),
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Закрепить/открепить чат
|
||
*/
|
||
async togglePinChat(chatId: number): Promise<any> {
|
||
return this.request(`/api/v1/communications/chats/${chatId}/pin/`, {
|
||
method: 'POST',
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Включить/выключить уведомления для чата
|
||
*/
|
||
async toggleChatNotifications(chatId: number): Promise<any> {
|
||
return this.request(`/api/v1/communications/chats/${chatId}/notifications/`, {
|
||
method: 'POST',
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Покинуть чат (для групповых чатов)
|
||
*/
|
||
async leaveChat(chatId: number): Promise<any> {
|
||
return this.request(`/api/v1/communications/chats/${chatId}/leave/`, {
|
||
method: 'POST',
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Удалить чат (требует прав администратора чата)
|
||
*/
|
||
async deleteChat(chatId: number): Promise<any> {
|
||
return this.request(`/api/v1/communications/chats/${chatId}/`, {
|
||
method: 'DELETE',
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Обновить информацию о чате (частичное обновление)
|
||
*/
|
||
async updateChat(chatId: number, data: Partial<{
|
||
name: string;
|
||
description: string;
|
||
type: string;
|
||
can_reply: boolean;
|
||
include_all_users: boolean;
|
||
flags: Record<string, any>;
|
||
extra_data: Record<string, any>;
|
||
}>): Promise<any> {
|
||
return this.request(`/api/v1/communications/chats/${chatId}/`, {
|
||
method: 'PATCH',
|
||
body: JSON.stringify(data),
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Загрузить аватар чата
|
||
*/
|
||
async uploadChatAvatar(chatId: number, file: File): Promise<any> {
|
||
const formData = new FormData();
|
||
formData.append('avatar', file);
|
||
|
||
return this.request(`/api/v1/communications/chats/${chatId}/`, {
|
||
method: 'PATCH',
|
||
body: formData,
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Добавить участника в чат
|
||
*/
|
||
async addChatMember(chatId: number, userId: number): Promise<any> {
|
||
return this.request(`/api/v1/communications/chats/${chatId}/add-member/`, {
|
||
method: 'POST',
|
||
body: JSON.stringify({ user_id: userId }),
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Удалить участника из чата
|
||
*/
|
||
async removeChatMember(chatId: number, userId: number): Promise<any> {
|
||
return this.request(`/api/v1/communications/chats/${chatId}/remove-member/`, {
|
||
method: 'POST',
|
||
body: JSON.stringify({ user_id: userId }),
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Изменить роль участника в чате
|
||
*/
|
||
async changeChatMemberRole(chatId: number, userId: number, role: 'admin' | 'moderator' | 'member' | 'guest'): Promise<any> {
|
||
return this.request(`/api/v1/communications/chats/${chatId}/change-role/`, {
|
||
method: 'POST',
|
||
body: JSON.stringify({ user_id: userId, role }),
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Обновить пользовательские настройки чата
|
||
*/
|
||
async updateChatUserSettings(chatId: number, data: Partial<{
|
||
custom_name: string;
|
||
is_hidden: boolean;
|
||
}>): Promise<any> {
|
||
// Примечание: может потребоваться отдельный endpoint на backend
|
||
// Сейчас используем PATCH чата, но лучше создать /api/v1/communications/chats/{id}/user-settings/
|
||
return this.request(`/api/v1/communications/chats/${chatId}/user-settings/`, {
|
||
method: 'PATCH',
|
||
body: JSON.stringify(data),
|
||
});
|
||
}
|
||
|
||
// ==================== Оборудование ====================
|
||
|
||
async getEquipmentCategories(params?: Record<string, string | number>): Promise<any> {
|
||
const queryParams = new URLSearchParams();
|
||
if (params) {
|
||
Object.entries(params).forEach(([key, value]) => {
|
||
if (value !== undefined && value !== null && value !== '') {
|
||
queryParams.append(key, String(value));
|
||
}
|
||
});
|
||
}
|
||
const url = `/api/v1/procurement/equipment-categories/${queryParams.toString() ? '?' + queryParams.toString() : ''}`;
|
||
return this.request(url);
|
||
}
|
||
|
||
async getEquipment(params?: Record<string, string | number>): Promise<any> {
|
||
const queryParams = new URLSearchParams();
|
||
if (params) {
|
||
Object.entries(params).forEach(([key, value]) => {
|
||
if (value !== undefined && value !== null && value !== '') {
|
||
queryParams.append(key, String(value));
|
||
}
|
||
});
|
||
}
|
||
const url = `/api/v1/procurement/equipment/${queryParams.toString() ? '?' + queryParams.toString() : ''}`;
|
||
return this.request(url);
|
||
}
|
||
|
||
async getEquipmentDetail(equipmentId: number): Promise<any> {
|
||
return this.request(`/api/v1/procurement/equipment/${equipmentId}/`);
|
||
}
|
||
|
||
async getMyEquipment(params?: Record<string, string | number>): Promise<any> {
|
||
const queryParams = new URLSearchParams();
|
||
if (params) {
|
||
Object.entries(params).forEach(([key, value]) => {
|
||
if (value !== undefined && value !== null && value !== '') {
|
||
queryParams.append(key, String(value));
|
||
}
|
||
});
|
||
}
|
||
const url = `/api/v1/procurement/equipment/my_equipment/${queryParams.toString() ? '?' + queryParams.toString() : ''}`;
|
||
return this.request(url);
|
||
}
|
||
|
||
async getWarrantyExpiringEquipment(): Promise<any> {
|
||
return this.request('/api/v1/procurement/equipment/warranty_expiring/');
|
||
}
|
||
|
||
async generateEquipmentInventoryNumber(): Promise<{ inventory_number: string }> {
|
||
return this.request('/api/v1/procurement/equipment/generate-inventory-number/');
|
||
}
|
||
|
||
async getEquipmentCreateOptions(): Promise<any> {
|
||
return this.request('/api/v1/procurement/equipment/create-options/');
|
||
}
|
||
|
||
async createEquipment(data: Record<string, any>): Promise<any> {
|
||
return this.request('/api/v1/procurement/equipment/', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(data),
|
||
});
|
||
}
|
||
|
||
async updateEquipment(equipmentId: number, data: Record<string, any>): Promise<any> {
|
||
return this.request(`/api/v1/procurement/equipment/${equipmentId}/`, {
|
||
method: 'PATCH',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(data),
|
||
});
|
||
}
|
||
|
||
async deleteEquipment(equipmentId: number): Promise<void> {
|
||
await this.request(`/api/v1/procurement/equipment/${equipmentId}/`, {
|
||
method: 'DELETE',
|
||
});
|
||
}
|
||
|
||
async transferEquipment(equipmentId: number, data: { to_department?: number | null; to_person?: number | null; reason?: string }): Promise<any> {
|
||
return this.request(`/api/v1/procurement/equipment/${equipmentId}/transfer/`, {
|
||
method: 'POST',
|
||
body: JSON.stringify(data),
|
||
});
|
||
}
|
||
|
||
async writeOffEquipment(equipmentId: number, reason: string): Promise<any> {
|
||
return this.request(`/api/v1/procurement/equipment/${equipmentId}/write_off/`, {
|
||
method: 'POST',
|
||
body: JSON.stringify({ reason }),
|
||
});
|
||
}
|
||
|
||
async addEquipmentMaintenance(equipmentId: number, data: { type: string; description?: string; cost?: string | number; date?: string }): Promise<any> {
|
||
return this.request(`/api/v1/procurement/equipment/${equipmentId}/add_maintenance/`, {
|
||
method: 'POST',
|
||
body: JSON.stringify(data),
|
||
});
|
||
}
|
||
|
||
async getEquipmentTransferHistory(equipmentId: number): Promise<any> {
|
||
return this.request(`/api/v1/procurement/equipment/${equipmentId}/transfer_history/`);
|
||
}
|
||
|
||
async getMaintenanceRecords(params?: Record<string, string | number>): Promise<any> {
|
||
const queryParams = new URLSearchParams();
|
||
if (params) {
|
||
Object.entries(params).forEach(([key, value]) => {
|
||
if (value !== undefined && value !== null && value !== '') {
|
||
queryParams.append(key, String(value));
|
||
}
|
||
});
|
||
}
|
||
const url = `/api/v1/procurement/maintenance/${queryParams.toString() ? '?' + queryParams.toString() : ''}`;
|
||
return this.request(url);
|
||
}
|
||
|
||
async getEquipmentCategoryTree(): Promise<any> {
|
||
return this.request('/api/v1/procurement/equipment-categories/tree/');
|
||
}
|
||
|
||
async getEquipmentCategoryChildren(categoryId: number): Promise<any> {
|
||
return this.request(`/api/v1/procurement/equipment-categories/${categoryId}/children/`);
|
||
}
|
||
|
||
async getEquipmentQrCodeBlobUrl(equipmentId: number): Promise<string> {
|
||
const headers: Record<string, string> = {};
|
||
const token = this.getToken();
|
||
if (token) {
|
||
headers['Authorization'] = `Bearer ${token}`;
|
||
}
|
||
|
||
const response = await fetch(`/api/v1/procurement/equipment/${equipmentId}/qr_code/`, {
|
||
method: 'GET',
|
||
headers,
|
||
});
|
||
|
||
if (!response.ok) {
|
||
throw new Error(`API Error: ${response.status} ${response.statusText}`);
|
||
}
|
||
|
||
const blob = await response.blob();
|
||
return URL.createObjectURL(blob);
|
||
}
|
||
|
||
async getEquipmentComments(equipmentId: number): Promise<any> {
|
||
return this.request(`/api/v1/procurement/equipment/${equipmentId}/comments/`);
|
||
}
|
||
|
||
async addEquipmentComment(equipmentId: number, text: string): Promise<any> {
|
||
return this.request(`/api/v1/procurement/equipment/${equipmentId}/comments/`, {
|
||
method: 'POST',
|
||
body: JSON.stringify({ text }),
|
||
});
|
||
}
|
||
|
||
async deleteEquipmentComment(equipmentId: number, commentId: number): Promise<void> {
|
||
await this.request(`/api/v1/procurement/equipment/${equipmentId}/comments/${commentId}/`, {
|
||
method: 'DELETE',
|
||
});
|
||
}
|
||
|
||
// ==================== Закупки (Procurement Requests) ====================
|
||
|
||
async getProcurementRequests(params?: Record<string, string | number>): Promise<any> {
|
||
const queryParams = new URLSearchParams();
|
||
if (params) {
|
||
Object.entries(params).forEach(([key, value]) => {
|
||
if (value !== undefined && value !== null && value !== '') {
|
||
queryParams.append(key, String(value));
|
||
}
|
||
});
|
||
}
|
||
const url = `/api/v1/procurement/requests/${queryParams.toString() ? '?' + queryParams.toString() : ''}`;
|
||
return this.request(url);
|
||
}
|
||
|
||
async getProcurementRequest(id: number): Promise<any> {
|
||
return this.request(`/api/v1/procurement/requests/${id}/`);
|
||
}
|
||
|
||
async createProcurementRequest(data: any): Promise<any> {
|
||
return this.request('/api/v1/procurement/requests/', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(data),
|
||
});
|
||
}
|
||
|
||
async updateProcurementRequest(id: number, data: any): Promise<any> {
|
||
return this.request(`/api/v1/procurement/requests/${id}/`, {
|
||
method: 'PATCH',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(data),
|
||
});
|
||
}
|
||
|
||
async deleteProcurementRequest(id: number): Promise<void> {
|
||
await this.request(`/api/v1/procurement/requests/${id}/`, {
|
||
method: 'DELETE',
|
||
});
|
||
}
|
||
|
||
async submitProcurementRequest(id: number): Promise<any> {
|
||
return this.request(`/api/v1/procurement/requests/${id}/submit/`, {
|
||
method: 'POST',
|
||
});
|
||
}
|
||
|
||
async approveProcurementRequest(id: number, comment?: string): Promise<any> {
|
||
return this.request(`/api/v1/procurement/requests/${id}/approve/`, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ comment: comment || '' }),
|
||
});
|
||
}
|
||
|
||
async rejectProcurementRequest(id: number, comment?: string): Promise<any> {
|
||
return this.request(`/api/v1/procurement/requests/${id}/reject/`, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ comment: comment || '' }),
|
||
});
|
||
}
|
||
|
||
async startWorkProcurementRequest(id: number): Promise<any> {
|
||
return this.request(`/api/v1/procurement/requests/${id}/start_work/`, {
|
||
method: 'POST',
|
||
});
|
||
}
|
||
|
||
async completeProcurementRequest(id: number): Promise<any> {
|
||
return this.request(`/api/v1/procurement/requests/${id}/complete/`, {
|
||
method: 'POST',
|
||
});
|
||
}
|
||
|
||
async cancelProcurementRequest(id: number, reason?: string): Promise<any> {
|
||
return this.request(`/api/v1/procurement/requests/${id}/cancel/`, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ reason: reason || '' }),
|
||
});
|
||
}
|
||
|
||
async getMyProcurementRequests(params?: Record<string, string | number>): Promise<any> {
|
||
const queryParams = new URLSearchParams();
|
||
if (params) {
|
||
Object.entries(params).forEach(([key, value]) => {
|
||
if (value !== undefined && value !== null && value !== '') {
|
||
queryParams.append(key, String(value));
|
||
}
|
||
});
|
||
}
|
||
const url = `/api/v1/procurement/requests/my_requests/${queryParams.toString() ? '?' + queryParams.toString() : ''}`;
|
||
return this.request(url);
|
||
}
|
||
|
||
async getPendingApprovals(params?: Record<string, string | number>): Promise<any> {
|
||
const queryParams = new URLSearchParams();
|
||
if (params) {
|
||
Object.entries(params).forEach(([key, value]) => {
|
||
if (value !== undefined && value !== null && value !== '') {
|
||
queryParams.append(key, String(value));
|
||
}
|
||
});
|
||
}
|
||
const url = `/api/v1/procurement/requests/pending_approvals/${queryParams.toString() ? '?' + queryParams.toString() : ''}`;
|
||
return this.request(url);
|
||
}
|
||
|
||
// ==================== Позиции закупок (Procurement Items) ====================
|
||
|
||
async getProcurementItems(params?: Record<string, string | number>): Promise<any> {
|
||
const queryParams = new URLSearchParams();
|
||
if (params) {
|
||
Object.entries(params).forEach(([key, value]) => {
|
||
if (value !== undefined && value !== null && value !== '') {
|
||
queryParams.append(key, String(value));
|
||
}
|
||
});
|
||
}
|
||
const url = `/api/v1/procurement/items/${queryParams.toString() ? '?' + queryParams.toString() : ''}`;
|
||
return this.request(url);
|
||
}
|
||
|
||
async createProcurementItem(data: any): Promise<any> {
|
||
return this.request('/api/v1/procurement/items/', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(data),
|
||
});
|
||
}
|
||
|
||
async updateProcurementItem(id: number, data: any): Promise<any> {
|
||
return this.request(`/api/v1/procurement/items/${id}/`, {
|
||
method: 'PATCH',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(data),
|
||
});
|
||
}
|
||
|
||
async deleteProcurementItem(id: number): Promise<void> {
|
||
await this.request(`/api/v1/procurement/items/${id}/`, {
|
||
method: 'DELETE',
|
||
});
|
||
}
|
||
|
||
// ==================== Поставщики закупок ====================
|
||
|
||
async getProcurementSuppliers(params?: Record<string, string | number>): Promise<any> {
|
||
const queryParams = new URLSearchParams();
|
||
if (params) {
|
||
Object.entries(params).forEach(([key, value]) => {
|
||
if (value !== undefined && value !== null && value !== '') {
|
||
queryParams.append(key, String(value));
|
||
}
|
||
});
|
||
}
|
||
const url = `/api/v1/procurement/suppliers/${queryParams.toString() ? '?' + queryParams.toString() : ''}`;
|
||
return this.request(url);
|
||
}
|
||
|
||
async createProcurementSupplier(data: any): Promise<any> {
|
||
return this.request('/api/v1/procurement/suppliers/', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(data),
|
||
});
|
||
}
|
||
|
||
async updateProcurementSupplier(id: number, data: any): Promise<any> {
|
||
return this.request(`/api/v1/procurement/suppliers/${id}/`, {
|
||
method: 'PATCH',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(data),
|
||
});
|
||
}
|
||
|
||
async deleteProcurementSupplier(id: number): Promise<void> {
|
||
await this.request(`/api/v1/procurement/suppliers/${id}/`, {
|
||
method: 'DELETE',
|
||
});
|
||
}
|
||
|
||
async getTopRatedProcurementSuppliers(): Promise<any> {
|
||
return this.request('/api/v1/procurement/suppliers/top_rated/');
|
||
}
|
||
|
||
// ==================== Статистика закупок ====================
|
||
|
||
async getProcurementOverviewStats(): Promise<any> {
|
||
return this.request('/api/v1/procurement/stats/overview/');
|
||
}
|
||
|
||
async getProcurementDepartmentStats(): Promise<any> {
|
||
return this.request('/api/v1/procurement/stats/by-department/');
|
||
}
|
||
}
|
||
|
||
export const apiClient = new ApiClient();
|
||
export default apiClient;
|