diff --git a/client/src/App.tsx b/client/src/App.tsx
index 012f516..f57cd22 100644
--- a/client/src/App.tsx
+++ b/client/src/App.tsx
@@ -1,6 +1,9 @@
+import { createTheme } from '@mui/material/styles';
+import { useMemo } from 'react';
import { Admin, Resource } from 'react-admin';
import { authProvider } from './auth/authProvider';
import { dataProvider } from './dataProvider';
+import { useEmbeddedParentTheme } from './embed/useEmbeddedParentTheme';
import { EmbeddedActiveEquipmentPage } from './pages/EmbeddedActiveEquipmentPage';
import { EquipmentCreate } from './resources/equipment/EquipmentCreate';
import { EquipmentEdit } from './resources/equipment/EquipmentEdit';
@@ -11,13 +14,18 @@ import { EquipmentStatusChangeEdit } from './resources/equipment-status-change/E
import { EquipmentStatusChangeList } from './resources/equipment-status-change/EquipmentStatusChangeList';
import { EquipmentStatusChangeShow } from './resources/equipment-status-change/EquipmentStatusChangeShow';
-function App() {
- if (window.location.pathname === '/embedded/equipment-active') {
- return ;
- }
+function ToirAdmin() {
+ const paletteMode = useEmbeddedParentTheme();
+ const theme = useMemo(
+ () =>
+ createTheme({
+ palette: { mode: paletteMode },
+ }),
+ [paletteMode],
+ );
return (
-
+
;
+ }
+
+ return ;
+}
+
export default App;
diff --git a/client/src/embed/useEmbeddedParentTheme.ts b/client/src/embed/useEmbeddedParentTheme.ts
new file mode 100644
index 0000000..3d3cee3
--- /dev/null
+++ b/client/src/embed/useEmbeddedParentTheme.ts
@@ -0,0 +1,48 @@
+import { useEffect, useState } from 'react';
+
+export type EmbedPaletteMode = 'light' | 'dark';
+
+const MESSAGE_TYPE = 'greact-theme' as const;
+
+function parseThemeFromSearch(): EmbedPaletteMode {
+ if (typeof window === 'undefined') {
+ return 'light';
+ }
+ const t = new URLSearchParams(window.location.search).get('theme');
+ return t === 'dark' ? 'dark' : 'light';
+}
+
+/** Comma-separated list of parent origins allowed to send theme via postMessage. If unset, all origins are accepted (theme-only payload). */
+function getAllowedOrigins(): string[] {
+ const raw = import.meta.env.VITE_TOIR_EMBED_PARENT_ORIGINS;
+ if (typeof raw === 'string' && raw.trim()) {
+ return raw
+ .split(',')
+ .map((s) => s.trim())
+ .filter(Boolean);
+ }
+ return [];
+}
+
+export function useEmbeddedParentTheme(): EmbedPaletteMode {
+ const [mode, setMode] = useState(parseThemeFromSearch);
+
+ useEffect(() => {
+ const allowed = getAllowedOrigins();
+ const handler = (event: MessageEvent) => {
+ if (allowed.length > 0 && !allowed.includes(event.origin)) {
+ return;
+ }
+ const data = event.data as { type?: string; theme?: string } | null;
+ if (data && typeof data === 'object' && data.type === MESSAGE_TYPE) {
+ if (data.theme === 'dark' || data.theme === 'light') {
+ setMode(data.theme);
+ }
+ }
+ };
+ window.addEventListener('message', handler);
+ return () => window.removeEventListener('message', handler);
+ }, []);
+
+ return mode;
+}
diff --git a/client/src/pages/EmbeddedActiveEquipmentPage.tsx b/client/src/pages/EmbeddedActiveEquipmentPage.tsx
index a33b3f2..f31ce48 100644
--- a/client/src/pages/EmbeddedActiveEquipmentPage.tsx
+++ b/client/src/pages/EmbeddedActiveEquipmentPage.tsx
@@ -1,6 +1,8 @@
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';
@@ -9,9 +11,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 { useEffect, useState } from 'react';
+import { ThemeProvider, createTheme } from '@mui/material/styles';
+import { useEffect, useMemo, useState } from 'react';
+import { useEmbeddedParentTheme } from '../embed/useEmbeddedParentTheme';
import { env } from '../config/env';
import { ensureFreshToken, getAccessToken } from '../auth/keycloak';
+import { downloadEquipmentAttachmentFile } from '../resources/equipment/attachmentDownload';
type EquipmentRecord = {
id: string;
@@ -19,11 +24,15 @@ type EquipmentRecord = {
serialNumber: string;
dateOfInspection: string | null;
commissionedAt: string | null;
+ attachment?: {
+ objectKey?: string;
+ originalFileName?: string | null;
+ } | null;
};
function formatDate(value: string | null) {
if (!value) {
- return '-';
+ return '—';
}
const date = new Date(value);
@@ -35,6 +44,24 @@ function formatDate(value: string | null) {
}
export function EmbeddedActiveEquipmentPage() {
+ const paletteMode = useEmbeddedParentTheme();
+ const muiTheme = useMemo(
+ () =>
+ createTheme({
+ palette: { mode: paletteMode },
+ components: {
+ MuiPaper: {
+ styleOverrides: {
+ root: {
+ backgroundImage: 'none',
+ },
+ },
+ },
+ },
+ }),
+ [paletteMode],
+ );
+
const [data, setData] = useState([]);
const [total, setTotal] = useState(null);
const [isPending, setIsPending] = useState(true);
@@ -105,52 +132,98 @@ export function EmbeddedActiveEquipmentPage() {
};
}, []);
- return (
-
-
-
-
- Оборудование в эксплуатации
-
-
- Отображаются записи со статусом `Active`
- {typeof total === 'number' ? `: ${total}` : ''}
-
-
+ 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';
- {isPending ? (
-
-
+ return (
+
+
+
+
+
+
+ Оборудование в эксплуатации
+
+
+ Отображаются записи со статусом 'Active'
+ {typeof total === 'number' ? `: ${total}` : ''}
+
- ) : error ? (
-
- Не удалось загрузить активное оборудование.
-
- ) : (
-
-
-
-
- Наименование
- Заводской номер
- Дата изготовления
- Дата поверки
-
-
-
- {(data ?? []).map((item) => (
-
- {item.name}
- {item.serialNumber}
- {formatDate(item.dateOfInspection)}
- {formatDate(item.commissionedAt)}
+
+ {isPending ? (
+
+
+
+ ) : error ? (
+
+ Не удалось загрузить активное оборудование.
+
+ ) : (
+
+
+
+
+ Наименование
+ Заводской номер
+ Дата изготовления
+ Дата поверки
+ Файл
- ))}
-
-
-
- )}
-
-
+
+
+ {(data ?? []).map((item) => (
+
+ {item.name}
+ {item.serialNumber}
+ {formatDate(item.commissionedAt)}
+ {formatDate(item.dateOfInspection)}
+
+ {item?.attachment?.objectKey ? (
+ {
+ 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() || 'Скачать'}
+
+ ) : (
+ '—'
+ )}
+
+
+ ))}
+
+
+
+ )}
+
+
+
);
}
diff --git a/client/src/vite-env.d.ts b/client/src/vite-env.d.ts
index 11f02fe..bf0c063 100644
--- a/client/src/vite-env.d.ts
+++ b/client/src/vite-env.d.ts
@@ -1 +1,9 @@
///
+
+interface ImportMetaEnv {
+ readonly VITE_TOIR_EMBED_PARENT_ORIGINS?: string;
+}
+
+interface ImportMeta {
+ readonly env: ImportMetaEnv;
+}