Add support for file attachments in equipment status changes

- Introduced functionality for uploading and managing attachments in the ChangeEquipmentStatus module.
- Added new endpoints for uploading and deleting attachments, as well as for downloading them.
- Updated the ChangeEquipmentStatusService to handle attachment storage and retrieval using the new storage service methods.
- Enhanced the ChangeEquipmentStatusEdit and ChangeEquipmentStatusShow components to support attachment input and display.
- Removed deprecated attachment handling from Equipment module to streamline functionality.
- Updated Prisma schema to reflect changes in attachment management.
This commit is contained in:
Первов Артем
2026-04-21 12:19:49 +03:00
parent d572647772
commit b1aefae2fa
25 changed files with 669 additions and 430 deletions

View File

@@ -2,7 +2,6 @@ import Alert from '@mui/material/Alert';
import Box from '@mui/material/Box';
import CircularProgress from '@mui/material/CircularProgress';
import CssBaseline from '@mui/material/CssBaseline';
import Link from '@mui/material/Link';
import Paper from '@mui/material/Paper';
import Table from '@mui/material/Table';
import TableBody from '@mui/material/TableBody';
@@ -11,12 +10,12 @@ import TableContainer from '@mui/material/TableContainer';
import TableHead from '@mui/material/TableHead';
import TableRow from '@mui/material/TableRow';
import Typography from '@mui/material/Typography';
import { ThemeProvider, createTheme } from '@mui/material/styles';
import { ThemeProvider } from '@mui/material/styles';
import { useEffect, useMemo, useState } from 'react';
import { useEmbeddedParentTheme } from '../embed/useEmbeddedParentTheme';
import { buildToirMuiTheme } from '../theme/toirMuiTheme';
import { env } from '../config/env';
import { ensureFreshToken, getAccessToken } from '../auth/keycloak';
import { downloadEquipmentAttachmentFile } from '../resources/equipment/attachmentDownload';
type EquipmentRecord = {
id: string;
@@ -24,10 +23,6 @@ type EquipmentRecord = {
serialNumber: string;
dateOfInspection: string | null;
commissionedAt: string | null;
attachment?: {
objectKey?: string;
originalFileName?: string | null;
} | null;
};
function formatDate(value: string | null) {
@@ -46,19 +41,7 @@ function formatDate(value: string | null) {
export function EmbeddedActiveEquipmentPage() {
const paletteMode = useEmbeddedParentTheme();
const muiTheme = useMemo(
() =>
createTheme({
palette: { mode: paletteMode },
components: {
MuiPaper: {
styleOverrides: {
root: {
backgroundImage: 'none',
},
},
},
},
}),
() => buildToirMuiTheme(paletteMode),
[paletteMode],
);
@@ -132,13 +115,6 @@ export function EmbeddedActiveEquipmentPage() {
};
}, []);
const pageBg =
paletteMode === 'dark' ? 'linear-gradient(180deg, #0d1117 0%, #0a0c10 100%)' : '#f3f6fa';
const headerBg = paletteMode === 'dark' ? '#161b22' : '#fff';
const headerBorder = paletteMode === 'dark' ? '1px solid #30363d' : '1px solid #d7e0ea';
const titleColor = paletteMode === 'dark' ? '#e6edf3' : '#10233a';
const subtitleColor = paletteMode === 'dark' ? '#8b949e' : '#5b7087';
return (
<ThemeProvider theme={muiTheme}>
<CssBaseline />
@@ -147,23 +123,34 @@ export function EmbeddedActiveEquipmentPage() {
minHeight: '100vh',
boxSizing: 'border-box',
p: { xs: 2, md: 3 },
bgcolor: pageBg,
background:
paletteMode === 'dark'
? 'radial-gradient(circle at 10% 10%, rgba(201, 122, 61, 0.18), transparent 40%), radial-gradient(circle at 90% 90%, rgba(212, 165, 116, 0.14), transparent 42%), #0a0d12'
: 'radial-gradient(circle at 12% 12%, rgba(182, 130, 81, 0.20), transparent 42%), radial-gradient(circle at 88% 88%, rgba(214, 188, 157, 0.26), transparent 44%), #f7f0e5',
}}
>
<Paper
elevation={0}
sx={{
overflow: 'hidden',
borderRadius: 3,
border: headerBorder,
bgcolor: paletteMode === 'dark' ? '#161b22' : '#fff',
borderRadius: 4,
}}
>
<Box sx={{ px: 3, py: 2, borderBottom: headerBorder, bgcolor: headerBg }}>
<Typography variant="h5" sx={{ fontWeight: 700, color: titleColor }}>
<Box
sx={{
px: 3,
py: 2.25,
borderBottom: (theme) => `1px solid ${theme.palette.divider}`,
background:
paletteMode === 'dark'
? 'linear-gradient(145deg, rgba(255,255,255,0.06), rgba(0,0,0,0.1))'
: 'linear-gradient(145deg, rgba(255,255,255,0.9), rgba(255,255,255,0.62))',
}}
>
<Typography variant="h5" sx={{ fontWeight: 600, letterSpacing: '-0.02em' }}>
Оборудование в эксплуатации
</Typography>
<Typography variant="body2" sx={{ mt: 0.5, color: subtitleColor }}>
<Typography variant="body2" sx={{ mt: 0.5, opacity: 0.85 }}>
Отображаются записи со статусом &apos;Active&apos;
{typeof total === 'number' ? `: ${total}` : ''}
</Typography>
@@ -186,7 +173,6 @@ export function EmbeddedActiveEquipmentPage() {
<TableCell sx={{ fontWeight: 700 }}>Заводской номер</TableCell>
<TableCell sx={{ fontWeight: 700 }}>Дата изготовления</TableCell>
<TableCell sx={{ fontWeight: 700 }}>Дата поверки</TableCell>
<TableCell sx={{ fontWeight: 700 }}>Файл</TableCell>
</TableRow>
</TableHead>
<TableBody>
@@ -196,26 +182,6 @@ export function EmbeddedActiveEquipmentPage() {
<TableCell>{item.serialNumber}</TableCell>
<TableCell>{formatDate(item.commissionedAt)}</TableCell>
<TableCell>{formatDate(item.dateOfInspection)}</TableCell>
<TableCell>
{item?.attachment?.objectKey ? (
<Link
component="button"
type="button"
underline="hover"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
const label = item.attachment?.originalFileName?.trim() || 'файл';
void downloadEquipmentAttachmentFile(item.id, label).catch(() => {});
}}
sx={{ cursor: 'pointer', verticalAlign: 'inherit' }}
>
{item.attachment?.originalFileName?.trim() || 'Скачать'}
</Link>
) : (
'—'
)}
</TableCell>
</TableRow>
))}
</TableBody>