Files
toir-light/client/src/pages/EmbeddedActiveEquipmentPage.tsx

197 lines
6.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 }}>
Отображаются записи со статусом &apos;Active&apos;
{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>
);
}