197 lines
6.5 KiB
TypeScript
197 lines
6.5 KiB
TypeScript
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 Paper from '@mui/material/Paper';
|
||
import Table from '@mui/material/Table';
|
||
import TableBody from '@mui/material/TableBody';
|
||
import TableCell from '@mui/material/TableCell';
|
||
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 } 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';
|
||
|
||
type EquipmentRecord = {
|
||
id: string;
|
||
name: string;
|
||
serialNumber: string;
|
||
dateOfInspection: string | null;
|
||
commissionedAt: string | null;
|
||
installationDate: string | null;
|
||
};
|
||
|
||
function formatDate(value: string | null) {
|
||
if (!value) {
|
||
return '—';
|
||
}
|
||
|
||
const date = new Date(value);
|
||
if (Number.isNaN(date.getTime())) {
|
||
return value;
|
||
}
|
||
|
||
return new Intl.DateTimeFormat('ru-RU').format(date);
|
||
}
|
||
|
||
export function EmbeddedActiveEquipmentPage() {
|
||
const paletteMode = useEmbeddedParentTheme();
|
||
const muiTheme = useMemo(
|
||
() => buildToirMuiTheme(paletteMode),
|
||
[paletteMode],
|
||
);
|
||
|
||
const [data, setData] = useState<EquipmentRecord[]>([]);
|
||
const [total, setTotal] = useState<number | null>(null);
|
||
const [isPending, setIsPending] = useState(true);
|
||
const [error, setError] = useState<Error | null>(null);
|
||
|
||
useEffect(() => {
|
||
let cancelled = false;
|
||
|
||
const load = async () => {
|
||
setIsPending(true);
|
||
setError(null);
|
||
|
||
try {
|
||
await ensureFreshToken();
|
||
|
||
const token = getAccessToken();
|
||
const headers = new Headers({
|
||
Accept: 'application/json',
|
||
});
|
||
|
||
if (token) {
|
||
headers.set('Authorization', `Bearer ${token}`);
|
||
}
|
||
|
||
const query = new URLSearchParams({
|
||
_start: '0',
|
||
_end: '100',
|
||
_sort: 'name',
|
||
_order: 'ASC',
|
||
});
|
||
query.append('status', 'Active');
|
||
|
||
const response = await fetch(`${env.apiUrl}/equipment?${query.toString()}`, {
|
||
headers,
|
||
});
|
||
|
||
if (!response.ok) {
|
||
const requestError = new Error(`Request failed with status ${response.status}`) as Error & {
|
||
status?: number;
|
||
};
|
||
requestError.status = response.status;
|
||
throw requestError;
|
||
}
|
||
|
||
const payload = (await response.json()) as EquipmentRecord[];
|
||
const contentRange = response.headers.get('Content-Range');
|
||
const parsedTotal = contentRange?.match(/\/(\d+)$/)?.[1];
|
||
|
||
if (!cancelled) {
|
||
setData(payload);
|
||
setTotal(parsedTotal ? Number(parsedTotal) : payload.length);
|
||
}
|
||
} catch (caughtError) {
|
||
if (!cancelled) {
|
||
setError(caughtError instanceof Error ? caughtError : new Error('Unknown error'));
|
||
}
|
||
} finally {
|
||
if (!cancelled) {
|
||
setIsPending(false);
|
||
}
|
||
}
|
||
};
|
||
|
||
void load();
|
||
|
||
return () => {
|
||
cancelled = true;
|
||
};
|
||
}, []);
|
||
|
||
return (
|
||
<ThemeProvider theme={muiTheme}>
|
||
<CssBaseline />
|
||
<Box
|
||
sx={{
|
||
minHeight: '100vh',
|
||
boxSizing: 'border-box',
|
||
p: { xs: 2, md: 3 },
|
||
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: 4,
|
||
}}
|
||
>
|
||
<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, opacity: 0.85 }}>
|
||
Отображаются записи со статусом 'Active'
|
||
{typeof total === 'number' ? `: ${total}` : ''}
|
||
</Typography>
|
||
</Box>
|
||
|
||
{isPending ? (
|
||
<Box sx={{ display: 'flex', justifyContent: 'center', py: 8 }}>
|
||
<CircularProgress />
|
||
</Box>
|
||
) : error ? (
|
||
<Box sx={{ p: 3 }}>
|
||
<Alert severity="error">Не удалось загрузить активное оборудование.</Alert>
|
||
</Box>
|
||
) : (
|
||
<TableContainer>
|
||
<Table size="small">
|
||
<TableHead>
|
||
<TableRow>
|
||
<TableCell sx={{ fontWeight: 700 }}>Наименование</TableCell>
|
||
<TableCell sx={{ fontWeight: 700 }}>Заводской номер</TableCell>
|
||
<TableCell sx={{ fontWeight: 700 }}>Дата установки</TableCell>
|
||
<TableCell sx={{ fontWeight: 700 }}>Дата поверки</TableCell>
|
||
</TableRow>
|
||
</TableHead>
|
||
<TableBody>
|
||
{(data ?? []).map((item) => (
|
||
<TableRow key={item.id} hover>
|
||
<TableCell>{item.name}</TableCell>
|
||
<TableCell>{item.serialNumber}</TableCell>
|
||
<TableCell>{formatDate(item.installationDate)}</TableCell>
|
||
<TableCell>{formatDate(item.dateOfInspection)}</TableCell>
|
||
</TableRow>
|
||
))}
|
||
</TableBody>
|
||
</Table>
|
||
</TableContainer>
|
||
)}
|
||
</Paper>
|
||
</Box>
|
||
</ThemeProvider>
|
||
);
|
||
}
|