mirror of
https://github.com/serty2005/rmser.git
synced 2026-02-04 19:02:33 -06:00
добавил архив фото, откуда можно удалить и посмотреть
This commit is contained in:
215
rmser-view/src/components/settings/PhotoStorageTab.tsx
Normal file
215
rmser-view/src/components/settings/PhotoStorageTab.tsx
Normal file
@@ -0,0 +1,215 @@
|
||||
import React, { useState } from "react";
|
||||
import {
|
||||
Card,
|
||||
Image,
|
||||
Button,
|
||||
Popconfirm,
|
||||
Tag,
|
||||
Pagination,
|
||||
Empty,
|
||||
Spin,
|
||||
message,
|
||||
Tooltip,
|
||||
} from "antd";
|
||||
import {
|
||||
DeleteOutlined,
|
||||
ReloadOutlined,
|
||||
FileImageOutlined,
|
||||
CheckCircleOutlined,
|
||||
FileTextOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { api, getStaticUrl } from "../../services/api";
|
||||
import { AxiosError } from "axios";
|
||||
import type { ReceiptPhoto, PhotoStatus } from "../../services/types";
|
||||
|
||||
export const PhotoStorageTab: React.FC = () => {
|
||||
const [page, setPage] = useState(1);
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const { data, isLoading, isError } = useQuery({
|
||||
queryKey: ["photos", page],
|
||||
queryFn: () => api.getPhotos(page, 18), // 18 - удобно делится на 2, 3, 6 колонок
|
||||
});
|
||||
|
||||
const deleteMutation = useMutation({
|
||||
mutationFn: ({ id, force }: { id: string; force: boolean }) =>
|
||||
api.deletePhoto(id, force),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["photos"] });
|
||||
message.success("Фото удалено");
|
||||
},
|
||||
// Исправленная типизация:
|
||||
onError: (error: AxiosError<{ error: string }>) => {
|
||||
if (error.response?.status === 409) {
|
||||
message.warning(
|
||||
"Это фото связано с черновиком. Используйте кнопку 'Удалить' с подтверждением."
|
||||
);
|
||||
} else {
|
||||
message.error(error.response?.data?.error || "Ошибка удаления");
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const regenerateMutation = useMutation({
|
||||
mutationFn: (id: string) => api.regenerateDraftFromPhoto(id),
|
||||
onSuccess: () => {
|
||||
message.success("Черновик восстановлен");
|
||||
// Можно редиректить, но пока просто обновим список
|
||||
},
|
||||
onError: () => {
|
||||
message.error("Ошибка восстановления");
|
||||
},
|
||||
});
|
||||
|
||||
const getStatusTag = (status: PhotoStatus) => {
|
||||
switch (status) {
|
||||
case "ORPHAN":
|
||||
return <Tag color="default">Без привязки</Tag>;
|
||||
case "HAS_DRAFT":
|
||||
return (
|
||||
<Tag icon={<FileTextOutlined />} color="processing">
|
||||
Черновик
|
||||
</Tag>
|
||||
);
|
||||
case "HAS_INVOICE":
|
||||
return (
|
||||
<Tag icon={<CheckCircleOutlined />} color="success">
|
||||
В iiko
|
||||
</Tag>
|
||||
);
|
||||
default:
|
||||
return <Tag>{status}</Tag>;
|
||||
}
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div style={{ textAlign: "center", padding: 40 }}>
|
||||
<Spin size="large" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (isError) {
|
||||
return <Empty description="Ошибка загрузки фото" />;
|
||||
}
|
||||
|
||||
if (!data?.photos?.length) {
|
||||
return <Empty description="Нет загруженных фото" />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div
|
||||
style={{
|
||||
display: "grid",
|
||||
gridTemplateColumns: "repeat(auto-fill, minmax(160px, 1fr))",
|
||||
gap: 16,
|
||||
marginBottom: 24,
|
||||
}}
|
||||
>
|
||||
{data.photos.map((photo: ReceiptPhoto) => (
|
||||
<Card
|
||||
key={photo.id}
|
||||
hoverable
|
||||
size="small"
|
||||
cover={
|
||||
<div
|
||||
style={{
|
||||
height: 160,
|
||||
overflow: "hidden",
|
||||
position: "relative",
|
||||
}}
|
||||
>
|
||||
<Image
|
||||
src={getStaticUrl(photo.file_url)}
|
||||
alt={photo.file_name}
|
||||
style={{ width: "100%", height: "100%", objectFit: "cover" }}
|
||||
preview={{ mask: <FileImageOutlined /> }}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
actions={[
|
||||
photo.can_regenerate ? (
|
||||
<Tooltip title="Создать черновик заново">
|
||||
<Button
|
||||
type="text"
|
||||
icon={<ReloadOutlined />}
|
||||
onClick={() => regenerateMutation.mutate(photo.id)}
|
||||
loading={regenerateMutation.isPending}
|
||||
size="small"
|
||||
/>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<span />
|
||||
), // Placeholder для выравнивания
|
||||
|
||||
photo.can_delete ? (
|
||||
<Popconfirm
|
||||
title="Удалить фото?"
|
||||
description={
|
||||
photo.status === "HAS_DRAFT"
|
||||
? "Внимание! Черновик тоже будет удален."
|
||||
: "Восстановить будет невозможно."
|
||||
}
|
||||
onConfirm={() =>
|
||||
deleteMutation.mutate({
|
||||
id: photo.id,
|
||||
force: photo.status === "HAS_DRAFT",
|
||||
})
|
||||
}
|
||||
okText="Удалить"
|
||||
cancelText="Отмена"
|
||||
okButtonProps={{ danger: true }}
|
||||
>
|
||||
<Button
|
||||
type="text"
|
||||
danger
|
||||
icon={<DeleteOutlined />}
|
||||
size="small"
|
||||
/>
|
||||
</Popconfirm>
|
||||
) : (
|
||||
<Tooltip title="Нельзя удалить: накладная уже в iiko">
|
||||
<Button
|
||||
type="text"
|
||||
disabled
|
||||
icon={<DeleteOutlined />}
|
||||
size="small"
|
||||
/>
|
||||
</Tooltip>
|
||||
),
|
||||
]}
|
||||
>
|
||||
<Card.Meta
|
||||
title={
|
||||
<div
|
||||
style={{
|
||||
fontSize: 12,
|
||||
whiteSpace: "nowrap",
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
}}
|
||||
>
|
||||
{new Date(photo.created_at).toLocaleDateString()}
|
||||
</div>
|
||||
}
|
||||
description={getStatusTag(photo.status)}
|
||||
/>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<Pagination
|
||||
current={page}
|
||||
total={data.total}
|
||||
pageSize={data.limit}
|
||||
onChange={setPage}
|
||||
showSizeChanger={false}
|
||||
style={{ textAlign: "center" }}
|
||||
simple
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -20,11 +20,13 @@ import {
|
||||
SettingOutlined,
|
||||
FolderOpenOutlined,
|
||||
TeamOutlined,
|
||||
CameraOutlined,
|
||||
} 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";
|
||||
import { PhotoStorageTab } from "../components/settings/PhotoStorageTab";
|
||||
|
||||
const { Title, Text } = Typography;
|
||||
|
||||
@@ -207,6 +209,16 @@ export const SettingsPage: React.FC = () => {
|
||||
});
|
||||
}
|
||||
|
||||
// Добавляем вкладку с фото (доступна для OWNER)
|
||||
if (currentUserRole === "OWNER") {
|
||||
tabsItems.push({
|
||||
key: "photos",
|
||||
label: "Архив фото",
|
||||
icon: <CameraOutlined />,
|
||||
children: <PhotoStorageTab />,
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ padding: "0 16px 80px" }}>
|
||||
<Title level={4} style={{ marginTop: 16 }}>
|
||||
|
||||
@@ -25,7 +25,8 @@ import type {
|
||||
UnifiedInvoice,
|
||||
ServerUser,
|
||||
UserRole,
|
||||
InvoiceDetails
|
||||
InvoiceDetails,
|
||||
GetPhotosResponse
|
||||
} from './types';
|
||||
|
||||
// Базовый URL
|
||||
@@ -255,4 +256,21 @@ export const api = {
|
||||
syncInvoices: async (): Promise<void> => {
|
||||
await apiClient.post('/invoices/sync');
|
||||
},
|
||||
};
|
||||
getPhotos: async (page = 1, limit = 20): Promise<GetPhotosResponse> => {
|
||||
const { data } = await apiClient.get<GetPhotosResponse>('/photos', {
|
||||
params: { page, limit }
|
||||
});
|
||||
return data;
|
||||
},
|
||||
|
||||
deletePhoto: async (id: string, force = false): Promise<void> => {
|
||||
await apiClient.delete(`/photos/${id}`, {
|
||||
params: { force }
|
||||
});
|
||||
},
|
||||
|
||||
regenerateDraftFromPhoto: async (id: string): Promise<void> => {
|
||||
await apiClient.post(`/photos/${id}/regenerate`);
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -274,4 +274,28 @@ export interface InvoiceDetails {
|
||||
total: number;
|
||||
}[];
|
||||
photo_url: string | null;
|
||||
}
|
||||
|
||||
export type PhotoStatus = 'ORPHAN' | 'HAS_DRAFT' | 'HAS_INVOICE';
|
||||
|
||||
export interface ReceiptPhoto {
|
||||
id: string;
|
||||
rms_server_id: string;
|
||||
uploaded_by: string;
|
||||
file_url: string;
|
||||
file_name: string;
|
||||
file_size: number;
|
||||
draft_id?: string;
|
||||
invoice_id?: string;
|
||||
created_at: string;
|
||||
status: PhotoStatus;
|
||||
can_delete: boolean;
|
||||
can_regenerate: boolean;
|
||||
}
|
||||
|
||||
export interface GetPhotosResponse {
|
||||
photos: ReceiptPhoto[];
|
||||
total: number;
|
||||
page: number;
|
||||
limit: number;
|
||||
}
|
||||
Reference in New Issue
Block a user