>(new Set());
+ // Состояние для просмотра фото чека
+ const [previewVisible, setPreviewVisible] = useState(false);
+
// --- ЗАПРОСЫ ---
const dictQuery = useQuery({
@@ -6344,20 +6562,17 @@ export const InvoiceDraftPage: React.FC = () => {
},
});
- // ДОБАВЛЕНО: Добавление строки
const addItemMutation = useMutation({
mutationFn: () => api.addDraftItem(id!),
onSuccess: () => {
message.success("Строка добавлена");
queryClient.invalidateQueries({ queryKey: ["draft", id] });
- // Можно сделать скролл вниз, но пока оставим как есть
},
onError: () => {
message.error("Ошибка создания строки");
},
});
- // ДОБАВЛЕНО: Удаление строки
const deleteItemMutation = useMutation({
mutationFn: (itemId: string) => api.deleteDraftItem(id!, itemId),
onSuccess: () => {
@@ -6542,16 +6757,30 @@ export const InvoiceDraftPage: React.FC = () => {
- : }
- onClick={handleDelete}
- loading={deleteDraftMutation.isPending}
- size="small"
- >
- {isCanceled ? "Удалить" : "Отмена"}
-
+ {/* Правая часть хедера: Кнопка чека и Кнопка удаления */}
+
+ {/* Кнопка просмотра чека (только если есть URL) */}
+ {draft.photo_url && (
+ }
+ onClick={() => setPreviewVisible(true)}
+ size="small"
+ >
+ Чек
+
+ )}
+
+ : }
+ onClick={handleDelete}
+ loading={deleteDraftMutation.isPending}
+ size="small"
+ >
+ {isCanceled ? "Удалить" : "Отмена"}
+
+
{/* Form: Склады и Поставщики */}
@@ -6671,7 +6900,7 @@ export const InvoiceDraftPage: React.FC = () => {
type="dashed"
block
icon={}
- style={{ marginTop: 12, marginBottom: 80, height: 48 }} // Увеличенный margin bottom для Affix
+ style={{ marginTop: 12, marginBottom: 80, height: 48 }}
onClick={() => addItemMutation.mutate()}
loading={addItemMutation.isPending}
disabled={isCanceled}
@@ -6725,6 +6954,22 @@ export const InvoiceDraftPage: React.FC = () => {
+
+ {/* Скрытый компонент для просмотра изображения */}
+ {draft.photo_url && (
+
+ setPreviewVisible(vis),
+ movable: true,
+ scaleStep: 0.5,
+ }}
+ >
+
+
+
+ )}
);
};
@@ -6809,16 +7054,19 @@ import {
TreeSelect,
Spin,
message,
+ Tabs,
} from "antd";
import {
SaveOutlined,
BarChartOutlined,
SettingOutlined,
FolderOpenOutlined,
+ TeamOutlined,
} from "@ant-design/icons";
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { api } from "../services/api";
import type { UserSettings } from "../services/types";
+import { TeamList } from "../components/settings/TeamList";
const { Title, Text } = Typography;
@@ -6892,6 +7140,115 @@ export const SettingsPage: React.FC = () => {
);
}
+ // Определяем роль текущего пользователя
+ const currentUserRole = settingsQuery.data?.role || "OPERATOR";
+ const showTeamSettings =
+ currentUserRole === "ADMIN" || currentUserRole === "OWNER";
+
+ // Сохраняем JSX в переменную вместо создания вложенного компонента
+ const generalSettingsContent = (
+
+ );
+
+ const tabsItems = [
+ {
+ key: "general",
+ label: "Общие",
+ icon: ,
+ children: generalSettingsContent,
+ },
+ ];
+
+ if (showTeamSettings) {
+ tabsItems.push({
+ key: "team",
+ label: "Команда",
+ icon: ,
+ children: (
+
+
+
+ ),
+ });
+ }
+
return (
@@ -6943,90 +7300,8 @@ export const SettingsPage: React.FC = () => {
- {/* Форма настроек */}
-
+ {/* Табы настроек */}
+
);
};
@@ -7062,11 +7337,21 @@ import type {
AddContainerRequest,
AddContainerResponse,
DictionariesResponse,
- DraftSummary
+ DraftSummary,
+ ServerUser,
+ UserRole
} from './types';
// Базовый URL
-const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:8080/api';
+export const API_BASE_URL = import.meta.env.VITE_API_URL || 'http://localhost:8080/api';
+
+// Хелпер для получения полного URL картинки (убирает /api если путь статики идет от корня, или добавляет как есть)
+// В данном ТЗ сказано просто склеивать.
+export const getStaticUrl = (path: string | null | undefined): string => {
+ if (!path) return '';
+ if (path.startsWith('http')) return path;
+ return `${API_BASE_URL}${path}`;
+};
// Телеграм объект
const tg = window.Telegram?.WebApp;
@@ -7075,7 +7360,7 @@ const tg = window.Telegram?.WebApp;
export const UNAUTHORIZED_EVENT = 'rms_unauthorized';
const apiClient = axios.create({
- baseURL: API_URL,
+ baseURL: API_BASE_URL,
headers: {
'Content-Type': 'application/json',
},
@@ -7243,6 +7528,23 @@ export const api = {
const { data } = await apiClient.get('/dictionaries/groups');
return data;
},
+
+ // --- Управление командой ---
+
+ getUsers: async (): Promise => {
+ const { data } = await apiClient.get('/settings/users');
+ return data;
+ },
+
+ updateUserRole: async (userId: string, newRole: UserRole): Promise<{ status: string }> => {
+ const { data } = await apiClient.patch<{ status: string }>(`/settings/users/${userId}`, { new_role: newRole });
+ return data;
+ },
+
+ removeUser: async (userId: string): Promise<{ status: string }> => {
+ const { data } = await apiClient.delete<{ status: string }>(`/settings/users/${userId}`);
+ return data;
+ },
};
```
@@ -7255,6 +7557,20 @@ export const api = {
export type UUID = string;
+// Добавляем типы ролей
+export type UserRole = 'OWNER' | 'ADMIN' | 'OPERATOR';
+
+// Интерфейс пользователя сервера
+export interface ServerUser {
+ user_id: string;
+ username: string; // @username или пустая строка
+ first_name: string;
+ last_name: string;
+ photo_url: string; // URL картинки или пустая строка
+ role: UserRole;
+ is_me: boolean; // Флаг, является ли этот юзер текущим пользователем
+}
+
// --- Каталог и Фасовки (API v2.0) ---
export interface ProductContainer {
@@ -7386,6 +7702,7 @@ export interface UserSettings {
root_group_id: UUID | null;
default_store_id: UUID | null;
auto_conduct: boolean;
+ role: UserRole; // Добавляем поле роли в настройки текущего пользователя
}
export interface InvoiceStats {
@@ -7449,6 +7766,7 @@ export interface DraftInvoice {
comment: string;
items: DraftItem[];
created_at?: string;
+ photo_url?: string; // Добавлено поле фото чека
}
// DTO для обновления строки