Files
Business-Management/frontend/src/api/client.ts
T
curo1305 94901fc30f Redesign doc service UX for scale + add group-based document sharing
- Three-column layout: Sidebar + SourcePanel (views + searchable category tree) + main
- DocumentSlideOver (480px right panel): inline editing, type picker, AI suggestion confirm/reject,
  categories combobox, tags editor, sharing section, raw text, re-analyse/delete actions
- ManageCategoriesDialog: inline rename, delete with confirm, search filter
- DocumentsPage rewrite: filter chip system, multi-file upload queue, drag-and-drop overlay,
  bulk actions bar (share/delete), smart TanStack Query polling, URL-driven view state
- Sidebar simplified: per-category NavLinks removed; Documents = single NavLink under Apps
- Backend: document_shares table (migration 0004), share CRUD endpoints, shared-with-me view,
  N+1-safe share_count via GROUP BY, recipient download access, X-User-Groups header enforcement
- Gateway proxy: injects X-User-Groups header into all document + category proxy requests
- Backend users: GET /api/users/me/groups endpoint for share picker combobox
- CLAUDE.md, STATUS.md files, and changelog updated

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-18 12:46:43 +02:00

470 lines
14 KiB
TypeScript

import axios from "axios";
const api = axios.create({ baseURL: "/api" });
api.interceptors.request.use((config) => {
const token = localStorage.getItem("token");
if (token) config.headers.Authorization = `Bearer ${token}`;
return config;
});
export default api;
// --- Auth ---
export const login = (email: string, password: string) =>
api
.post<{ access_token: string }>("/auth/login", new URLSearchParams({ username: email, password }))
.then((r) => r.data.access_token);
export const register = (email: string, password: string, full_name?: string) =>
api.post("/auth/register", { email, password, full_name }).then((r) => r.data);
// --- Users ---
export interface UserData {
id: string;
email: string;
full_name: string | null;
is_active: boolean;
is_admin: boolean;
color_mode: string | null;
}
export const getMe = () => api.get<UserData>("/users/me").then((r) => r.data);
export interface DashboardPrefs {
app_ids: string[];
}
export const getDashboardPrefs = () =>
api.get<DashboardPrefs>("/users/me/preferences").then((r) => r.data);
export const updateDashboardPrefs = (app_ids: string[]) =>
api.patch<DashboardPrefs>("/users/me/preferences", { app_ids }).then((r) => r.data);
// --- Admin ---
export interface AdminUserCreate {
email: string;
password: string;
full_name?: string;
is_admin?: boolean;
}
export const adminGetUsers = () =>
api.get<UserData[]>("/admin/users").then((r) => r.data);
export const adminCreateUser = (data: AdminUserCreate) =>
api.post<UserData>("/admin/users", data).then((r) => r.data);
export const adminDeleteUser = (userId: string) =>
api.delete(`/admin/users/${userId}`);
export const adminToggleActive = (userId: string) =>
api.patch<UserData>(`/admin/users/${userId}/active`).then((r) => r.data);
// --- Profile ---
export interface ProfileData {
id: string;
user_id: string;
phone: string | null;
date_of_birth: string | null;
position: string | null;
address: string | null;
updated_at: string;
}
export interface ProfileUpdate {
phone?: string | null;
date_of_birth?: string | null;
position?: string | null;
address?: string | null;
}
export const getProfile = () =>
api.get<ProfileData>("/profile/me").then((r) => r.data);
export const updateProfile = (data: ProfileUpdate) =>
api.put<ProfileData>("/profile/me", data).then((r) => r.data);
// --- Documents ---
export type DocumentStatus = "pending" | "processing" | "done" | "failed";
export interface CategoryOut {
id: string;
name: string;
}
export interface DocumentOut {
id: string;
user_id: string;
filename: string;
title: string | null;
file_size: number;
status: DocumentStatus;
document_type: string | null;
extracted_data: string | null;
tags: string | null;
error_message: string | null;
created_at: string;
processed_at: string | null;
categories: CategoryOut[];
source: string;
watch_path: string | null;
suggested_folder: string | null;
suggested_filename: string | null;
share_count: number;
}
export interface SharedDocumentOut extends DocumentOut {
shared_by_user_id: string;
shared_via_group_id: string;
}
export interface DocumentShareOut {
id: string;
document_id: string;
group_id: string;
shared_by_user_id: string;
created_at: string;
}
export interface DocumentPage {
items: DocumentOut[];
total: number;
page: number;
pages: number;
}
export interface DocumentListParams {
page?: number;
per_page?: number;
sort?: string;
order?: "asc" | "desc";
status?: string;
document_type?: string;
search?: string;
category_id?: string;
}
export interface DocumentStatusOut {
id: string;
status: DocumentStatus;
document_type: string | null;
error_message: string | null;
processed_at: string | null;
}
export const listDocuments = (params: DocumentListParams = {}) =>
api.get<DocumentPage>("/documents", { params }).then((r) => r.data);
export const listSharedWithMe = (params: DocumentListParams = {}) =>
api.get<DocumentPage>("/documents/shared-with-me", { params }).then((r) => r.data);
export const getDocumentShares = (docId: string) =>
api.get<DocumentShareOut[]>(`/documents/${docId}/shares`).then((r) => r.data);
export const addDocumentShare = (docId: string, groupId: string) =>
api.post<DocumentShareOut>(`/documents/${docId}/shares`, { group_id: groupId }).then((r) => r.data);
export const removeDocumentShare = (docId: string, groupId: string) =>
api.delete(`/documents/${docId}/shares/${groupId}`);
export const getDocument = (id: string) =>
api.get<DocumentOut>(`/documents/${id}`).then((r) => r.data);
export const getDocumentStatus = (id: string) =>
api.get<DocumentStatusOut>(`/documents/${id}/status`).then((r) => r.data);
export const uploadDocument = (file: File) => {
const form = new FormData();
form.append("file", file);
return api.post<DocumentOut>("/documents/upload", form).then((r) => r.data);
};
export const updateDocumentType = (id: string, document_type: string) =>
api.patch<DocumentOut>(`/documents/${id}/type`, { document_type }).then((r) => r.data);
export const deleteDocument = (id: string) =>
api.delete(`/documents/${id}`);
export const downloadDocument = async (id: string, filename: string) => {
const response = await api.get(`/documents/${id}/file`, { responseType: "blob" });
const url = URL.createObjectURL(response.data);
const a = document.createElement("a");
a.href = url;
a.download = filename;
a.click();
URL.revokeObjectURL(url);
};
export const viewDocument = async (id: string): Promise<void> => {
const response = await api.get(`/documents/${id}/file`, { responseType: "blob" });
const url = URL.createObjectURL(response.data);
const win = window.open(url, "_blank");
// Revoke after a generous delay — the new tab needs time to load the blob
setTimeout(() => URL.revokeObjectURL(url), 60_000);
if (!win) alert("Pop-up blocked. Please allow pop-ups for this site to preview PDFs.");
};
export const updateDocumentTags = (id: string, tags: string[]) =>
api.patch<DocumentOut>(`/documents/${id}/tags`, { tags }).then((r) => r.data);
export const updateDocumentTitle = (id: string, title: string) =>
api.patch<DocumentOut>(`/documents/${id}/title`, { title }).then((r) => r.data);
export const reprocessDocument = (id: string) =>
api.post<DocumentOut>(`/documents/${id}/reprocess`).then((r) => r.data);
export const assignCategory = (docId: string, catId: string) =>
api.post(`/documents/${docId}/categories/${catId}`);
export const removeCategory = (docId: string, catId: string) =>
api.delete(`/documents/${docId}/categories/${catId}`);
// --- Categories ---
export const listCategories = () =>
api.get<CategoryOut[]>("/documents/categories").then((r) => r.data);
export const createCategory = (name: string) =>
api.post<CategoryOut>("/documents/categories", { name }).then((r) => r.data);
export const renameCategory = (id: string, name: string) =>
api.patch<CategoryOut>(`/documents/categories/${id}`, { name }).then((r) => r.data);
export const deleteCategory = (id: string) =>
api.delete(`/documents/categories/${id}`);
// --- Appearance & Themes ---
export interface ThemeColors {
primary: string;
primary_hover: string;
accent: string;
accent_hover: string;
background: string;
surface: string;
border: string;
text_primary: string;
text_muted: string;
}
export interface ThemeDefinition {
id: string;
label: string;
builtin: boolean;
light: ThemeColors;
dark: ThemeColors;
}
export interface AppearanceSettings {
theme: string;
default_mode: string;
}
export const getAppearanceSettings = (): Promise<AppearanceSettings> =>
api.get<AppearanceSettings>("/settings/appearance").then((r) => r.data);
export const updateAppearanceSettings = (data: AppearanceSettings): Promise<AppearanceSettings> =>
api.patch<AppearanceSettings>("/settings/appearance", data).then((r) => r.data);
export const getThemes = (): Promise<ThemeDefinition[]> =>
api.get<ThemeDefinition[]>("/settings/themes").then((r) => r.data);
export const createTheme = (data: Omit<ThemeDefinition, "builtin">): Promise<ThemeDefinition> =>
api.post<ThemeDefinition>("/settings/themes", data).then((r) => r.data);
export const updateTheme = (
id: string,
data: { label?: string; light?: ThemeColors; dark?: ThemeColors }
): Promise<ThemeDefinition> =>
api.patch<ThemeDefinition>(`/settings/themes/${id}`, data).then((r) => r.data);
export const deleteTheme = (id: string): Promise<void> =>
api.delete(`/settings/themes/${id}`).then((r) => r.data);
export const updateColorMode = (color_mode: string): Promise<UserData> =>
api.patch<UserData>("/users/me/color-mode", { color_mode }).then((r) => r.data);
// --- Settings (admin only) ---
export interface AIProviderUpdate {
provider: string;
anthropic_api_key?: string;
anthropic_model?: string;
ollama_base_url?: string;
ollama_model?: string;
ollama_api_key?: string;
lmstudio_base_url?: string;
lmstudio_model?: string;
lmstudio_api_key?: string;
}
export const getAISettings = () =>
api.get<Record<string, unknown>>("/settings/ai").then((r) => r.data);
export const updateAISettings = (data: AIProviderUpdate) =>
api.patch<Record<string, unknown>>("/settings/ai", data).then((r) => r.data);
export const testAIConnection = () =>
api.post<{ ok: boolean; provider: string; response?: string; error?: string }>(
"/settings/ai/test"
).then((r) => r.data);
export const updateDocumentLimits = (max_pdf_mb: number) =>
api.patch<Record<string, unknown>>("/settings/documents/limits", { max_pdf_mb }).then(
(r) => r.data
);
export const getDocumentLimits = () =>
api.get<Record<string, unknown>>("/settings/documents/limits").then((r) => r.data);
// --- User groups (current user's own memberships) ---
export interface UserGroupOut {
id: string;
name: string;
description: string | null;
}
export const getMyGroups = () =>
api.get<UserGroupOut[]>("/users/me/groups").then((r) => r.data);
// --- Groups (admin only) ---
export interface GroupOut {
id: string;
name: string;
description: string | null;
created_at: string;
member_count: number;
}
export interface GroupMemberOut {
id: string;
email: string;
full_name: string | null;
is_active: boolean;
joined_at: string;
}
export interface GroupDetailOut extends GroupOut {
members: GroupMemberOut[];
}
export interface GroupCreate {
name: string;
description?: string | null;
}
export interface GroupUpdate {
name?: string;
description?: string | null;
}
export const adminListGroups = () =>
api.get<GroupOut[]>("/admin/groups").then((r) => r.data);
export const adminCreateGroup = (data: GroupCreate) =>
api.post<GroupOut>("/admin/groups", data).then((r) => r.data);
export const adminGetGroup = (groupId: string) =>
api.get<GroupDetailOut>(`/admin/groups/${groupId}`).then((r) => r.data);
export const adminUpdateGroup = (groupId: string, data: GroupUpdate) =>
api.patch<GroupOut>(`/admin/groups/${groupId}`, data).then((r) => r.data);
export const adminDeleteGroup = (groupId: string) =>
api.delete(`/admin/groups/${groupId}`);
export const adminAddGroupMember = (groupId: string, userId: string) =>
api.post(`/admin/groups/${groupId}/members/${userId}`);
export const adminRemoveGroupMember = (groupId: string, userId: string) =>
api.delete(`/admin/groups/${groupId}/members/${userId}`);
// --- Services ---
export interface ServiceStatus {
id: string;
name: string;
description: string;
healthy: boolean;
app_path: string;
settings_path: string;
}
export const getServices = () =>
api.get<ServiceStatus[]>("/services").then((r) => r.data);
// --- System Prompts (admin only) ---
export interface ServiceSystemPrompt {
label: string;
system: string;
user_template: string;
default_system: string;
default_user_template: string;
}
export type SystemPromptsData = Record<string, ServiceSystemPrompt>;
export const getSystemPrompts = () =>
api.get<SystemPromptsData>("/settings/system-prompts").then((r) => r.data);
export const updateSystemPrompt = (
serviceId: string,
data: { system: string; user_template: string }
) =>
api
.patch<SystemPromptsData>(`/settings/system-prompts/${serviceId}`, data)
.then((r) => r.data);
// --- Document suggestions (watch-ingested documents) ---
export const confirmFolderSuggestion = (docId: string) =>
api.post(`/documents/${docId}/suggestions/folder/confirm`);
export const rejectFolderSuggestion = (docId: string) =>
api.post(`/documents/${docId}/suggestions/folder/reject`);
export const confirmFilenameSuggestion = (docId: string) =>
api.post(`/documents/${docId}/suggestions/filename/confirm`);
export const rejectFilenameSuggestion = (docId: string) =>
api.post(`/documents/${docId}/suggestions/filename/reject`);
// --- Plugins ---
export interface PluginOut {
id: string;
name: string;
icon: string;
version: string;
}
export interface PluginSchemaProperty {
type: string;
title: string;
description?: string;
readOnly?: boolean;
}
export interface PluginManifest {
id: string;
name: string;
icon: string;
version: string;
access: {
allow_superuser: boolean;
required_groups: string[];
};
settings_schema: {
type: string;
title?: string;
properties: Record<string, PluginSchemaProperty>;
};
}
export const getPlugins = () =>
api.get<PluginOut[]>("/plugins").then((r) => r.data);
export const getPluginManifest = (id: string) =>
api.get<PluginManifest>(`/plugins/${id}/manifest`).then((r) => r.data);
export const getPluginSettings = (id: string) =>
api.get<Record<string, unknown>>(`/plugins/${id}/settings`).then((r) => r.data);
export const updatePluginSettings = (id: string, data: Record<string, unknown>) =>
api.patch<Record<string, unknown>>(`/plugins/${id}/settings`, data).then((r) => r.data);