u
This commit is contained in:
@@ -8,6 +8,7 @@ import ButtonBase from '@mui/material/ButtonBase';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import { Outlet, useLocation, useNavigate } from 'react-router-dom';
|
||||
import UserMenu from './UserMenu';
|
||||
import OnlinePrinterIcons from './OnlinePrinterIcons';
|
||||
import HelpBadge from './HelpBadge';
|
||||
import { NAV_ITEMS, SUB_NAV, resolveActiveNav, resolveActiveSub } from '../nav';
|
||||
import { artikelQueryPath, buildSearchUrl } from './article/artikelQuery';
|
||||
@@ -198,7 +199,18 @@ class AppSidebar extends PureComponent {
|
||||
})}
|
||||
</List>
|
||||
|
||||
<Box sx={{ ml: { xs: 'auto', lg: 0 }, flexShrink: 0, display: 'flex', alignItems: 'center' }}>
|
||||
<Box
|
||||
sx={{
|
||||
ml: { xs: 'auto', lg: 0 },
|
||||
flexShrink: 0,
|
||||
display: 'flex',
|
||||
flexDirection: { xs: 'row', lg: 'column' },
|
||||
alignItems: { xs: 'center', lg: 'flex-start' },
|
||||
width: { xs: 'auto', lg: '100%' },
|
||||
gap: { xs: 0.5, lg: 0 },
|
||||
}}
|
||||
>
|
||||
<OnlinePrinterIcons />
|
||||
<UserMenu />
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
290
src/components/OnlinePrinterIcons.jsx
Normal file
290
src/components/OnlinePrinterIcons.jsx
Normal file
@@ -0,0 +1,290 @@
|
||||
import { PureComponent } from 'react';
|
||||
import Badge from '@mui/material/Badge';
|
||||
import Box from '@mui/material/Box';
|
||||
import Chip from '@mui/material/Chip';
|
||||
import Divider from '@mui/material/Divider';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import ListItemIcon from '@mui/material/ListItemIcon';
|
||||
import ListItemText from '@mui/material/ListItemText';
|
||||
import Menu from '@mui/material/Menu';
|
||||
import MenuItem from '@mui/material/MenuItem';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import PrintOutlinedIcon from '@mui/icons-material/PrintOutlined';
|
||||
import SettingsIcon from '@mui/icons-material/Settings';
|
||||
import { enqueueSnackbar } from 'notistack';
|
||||
import companyAutomat from '../automats/companyAutomat';
|
||||
import { notifyError } from '../utils/notify';
|
||||
|
||||
export default class OnlinePrinterIcons extends PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
agentprinters: Array.isArray(companyAutomat.state.agentprinters)
|
||||
? companyAutomat.state.agentprinters
|
||||
: [],
|
||||
livePrinters: Array.isArray(companyAutomat.state.livePrinters)
|
||||
? companyAutomat.state.livePrinters
|
||||
: [],
|
||||
menuAnchor: null,
|
||||
testingId: null,
|
||||
};
|
||||
this.handleOpenMenu = this.handleOpenMenu.bind(this);
|
||||
this.handleCloseMenu = this.handleCloseMenu.bind(this);
|
||||
this.handleTestPrint = this.handleTestPrint.bind(this);
|
||||
this.handleOpenPrinterSettings = this.handleOpenPrinterSettings.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.unsub = companyAutomat.subscribe(this, (state) => ({
|
||||
agentprinters: Array.isArray(state.agentprinters) ? state.agentprinters : [],
|
||||
livePrinters: Array.isArray(state.livePrinters) ? state.livePrinters : [],
|
||||
}));
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.unsub?.();
|
||||
}
|
||||
|
||||
getOnlinePrinters() {
|
||||
const { agentprinters = [], livePrinters = [] } = this.state;
|
||||
const liveBySerial = new Map();
|
||||
for (const lp of livePrinters) {
|
||||
if (lp.serial) liveBySerial.set(String(lp.serial).toLowerCase(), lp);
|
||||
}
|
||||
|
||||
if (agentprinters.length > 0) {
|
||||
return agentprinters
|
||||
.filter((p) => {
|
||||
if (p.online === true) return true;
|
||||
const live =
|
||||
(p.serial ? liveBySerial.get(String(p.serial).toLowerCase()) : null) ??
|
||||
livePrinters.find(
|
||||
(lp) =>
|
||||
(lp.agentprinterId && lp.agentprinterId === p.id) ||
|
||||
(lp.agentId === p.printeragentId && lp.serial === p.serial),
|
||||
);
|
||||
if (!live) return false;
|
||||
return live.status !== 'offline' && !live.unreachable && !live.errors?.unreachable;
|
||||
})
|
||||
.map((p) => {
|
||||
const live =
|
||||
(p.serial ? liveBySerial.get(String(p.serial).toLowerCase()) : null) ??
|
||||
livePrinters.find(
|
||||
(lp) =>
|
||||
(lp.agentprinterId && lp.agentprinterId === p.id) ||
|
||||
(lp.agentId === p.printeragentId && lp.serial === p.serial),
|
||||
);
|
||||
return { ...p, live };
|
||||
});
|
||||
}
|
||||
|
||||
return livePrinters
|
||||
.filter((lp) => lp.status !== 'offline' && !lp.unreachable && !lp.errors?.unreachable)
|
||||
.map((lp) => ({
|
||||
id: lp.agentprinterId || lp.id || lp.serial,
|
||||
model: lp.model,
|
||||
serial: lp.serial,
|
||||
type: lp.type,
|
||||
online: true,
|
||||
open: Boolean(lp.errors?.headOpen || lp.headOpen),
|
||||
pause: Boolean(lp.errors?.pause || lp.pause),
|
||||
error: Boolean(lp.errors?.other || lp.error),
|
||||
labelLength: lp.labelLength,
|
||||
live: lp,
|
||||
}));
|
||||
}
|
||||
|
||||
handleOpenMenu(event, printer) {
|
||||
this.setState({
|
||||
menuAnchor: {
|
||||
el: event.currentTarget,
|
||||
printer,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
handleCloseMenu() {
|
||||
this.setState({ menuAnchor: null });
|
||||
}
|
||||
|
||||
async handleTestPrint(printer) {
|
||||
if (!printer?.id) return;
|
||||
this.setState({ testingId: printer.id });
|
||||
try {
|
||||
await companyAutomat.actions.sendTestPrint(printer.id, {
|
||||
model: printer.model || printer.type || 'Zebra',
|
||||
});
|
||||
enqueueSnackbar(
|
||||
`Testdruck für ${printer.model || 'Drucker'} (#${printer.id}) an Warteschlange gesendet.`,
|
||||
{ variant: 'success' },
|
||||
);
|
||||
} catch (err) {
|
||||
notifyError(err);
|
||||
} finally {
|
||||
this.setState({ testingId: null, menuAnchor: null });
|
||||
}
|
||||
}
|
||||
|
||||
handleOpenPrinterSettings() {
|
||||
this.handleCloseMenu();
|
||||
window.dispatchEvent(
|
||||
new CustomEvent('open-settings', { detail: { tab: 'printeragent' } }),
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { menuAnchor, testingId } = this.state;
|
||||
const onlinePrinters = this.getOnlinePrinters();
|
||||
|
||||
if (onlinePrinters.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const activePrinter = menuAnchor?.printer;
|
||||
const activeHasError = Boolean(
|
||||
activePrinter?.error ||
|
||||
activePrinter?.open ||
|
||||
activePrinter?.live?.errors?.other ||
|
||||
activePrinter?.live?.errors?.headOpen,
|
||||
);
|
||||
const activeIsPaused = Boolean(activePrinter?.pause || activePrinter?.live?.errors?.pause);
|
||||
const activeStatusText = activeHasError
|
||||
? activePrinter?.open || activePrinter?.live?.errors?.headOpen
|
||||
? 'Druckkopf offen'
|
||||
: 'Fehler'
|
||||
: activeIsPaused
|
||||
? 'Pausiert'
|
||||
: 'Bereit';
|
||||
|
||||
const anchorEl = menuAnchor?.el;
|
||||
const isTop = Boolean(
|
||||
anchorEl && typeof anchorEl.getBoundingClientRect === 'function' && anchorEl.getBoundingClientRect().top < 200
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: { xs: 'row', lg: 'column' },
|
||||
alignItems: { xs: 'center', lg: 'flex-start' },
|
||||
alignSelf: { xs: 'center', lg: 'flex-start' },
|
||||
px: { xs: 0.5, sm: 1, lg: 1.5 },
|
||||
py: { xs: 0.5, lg: 0.5 },
|
||||
gap: { xs: 0.5, lg: 1 },
|
||||
}}
|
||||
>
|
||||
{onlinePrinters.map((p) => {
|
||||
const hasError = Boolean(
|
||||
p.error || p.open || p.live?.errors?.other || p.live?.errors?.headOpen,
|
||||
);
|
||||
const isPaused = Boolean(p.pause || p.live?.errors?.pause);
|
||||
const label = p.model || 'Drucker';
|
||||
const statusText = hasError
|
||||
? p.open || p.live?.errors?.headOpen
|
||||
? 'Druckkopf offen'
|
||||
: 'Fehler'
|
||||
: isPaused
|
||||
? 'Pausiert'
|
||||
: 'Bereit';
|
||||
const tooltipTitle = `${label}${p.serial ? ` (${p.serial})` : ''} • ${statusText}`;
|
||||
|
||||
return (
|
||||
<IconButton
|
||||
onClick={(e) => this.handleOpenMenu(e, p)}
|
||||
sx={(theme) => ({
|
||||
p: 0,
|
||||
width: 32,
|
||||
height: 32,
|
||||
borderRadius: '50%',
|
||||
color: theme.nav?.item || 'inherit',
|
||||
'&:hover': {
|
||||
color: theme.nav?.itemHoverText || 'inherit',
|
||||
bgcolor: theme.nav?.itemHover || 'action.hover',
|
||||
},
|
||||
})}
|
||||
aria-label={`Drucker ${label}`}
|
||||
>
|
||||
<Badge
|
||||
variant="dot"
|
||||
color={hasError ? 'error' : isPaused ? 'warning' : 'success'}
|
||||
overlap="circular"
|
||||
anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }}
|
||||
>
|
||||
<PrintOutlinedIcon sx={{ fontSize: 20 }} />
|
||||
</Badge>
|
||||
</IconButton>
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
|
||||
<Menu
|
||||
anchorEl={anchorEl}
|
||||
open={Boolean(menuAnchor)}
|
||||
onClose={this.handleCloseMenu}
|
||||
anchorOrigin={{
|
||||
vertical: isTop ? 'bottom' : 'top',
|
||||
horizontal: isTop ? 'right' : 'left',
|
||||
}}
|
||||
transformOrigin={{
|
||||
vertical: isTop ? 'top' : 'bottom',
|
||||
horizontal: isTop ? 'right' : 'left',
|
||||
}}
|
||||
>
|
||||
{activePrinter && (
|
||||
<Box sx={{ px: 2, py: 1, minWidth: 210 }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 1 }}>
|
||||
<Typography variant="subtitle2" fontWeight={600} noWrap>
|
||||
{activePrinter.model || 'Drucker'}
|
||||
</Typography>
|
||||
<Chip
|
||||
label={activeStatusText}
|
||||
size="small"
|
||||
color={activeHasError ? 'error' : activeIsPaused ? 'warning' : 'success'}
|
||||
variant="outlined"
|
||||
sx={{ fontSize: '0.65rem', height: 18, fontWeight: 600 }}
|
||||
/>
|
||||
</Box>
|
||||
{activePrinter.serial ? (
|
||||
<Typography
|
||||
variant="caption"
|
||||
color="text.secondary"
|
||||
sx={{ fontFamily: 'monospace', display: 'block' }}
|
||||
>
|
||||
S/N: {activePrinter.serial}
|
||||
</Typography>
|
||||
) : null}
|
||||
{activePrinter.labelLength ? (
|
||||
<Typography variant="caption" color="text.secondary" display="block">
|
||||
Etikett: {activePrinter.labelLength} px
|
||||
</Typography>
|
||||
) : null}
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<Divider />
|
||||
|
||||
<MenuItem
|
||||
onClick={() => void this.handleTestPrint(activePrinter)}
|
||||
disabled={testingId === activePrinter?.id}
|
||||
>
|
||||
<ListItemIcon>
|
||||
<PrintOutlinedIcon fontSize="small" color="primary" />
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
primary={testingId === activePrinter?.id ? 'Sendet Testdruck…' : 'Testdruck senden'}
|
||||
secondary="Minimales ZPL-Etikett"
|
||||
/>
|
||||
</MenuItem>
|
||||
|
||||
<MenuItem onClick={this.handleOpenPrinterSettings}>
|
||||
<ListItemIcon>
|
||||
<SettingsIcon fontSize="small" />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary="Drucker-Einstellungen" />
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -25,6 +25,7 @@ class UserMenu extends PureComponent {
|
||||
this.state = {
|
||||
anchorEl: null,
|
||||
settingsOpen: false,
|
||||
settingsInitialTab: null,
|
||||
user: authAutomat.state.user,
|
||||
helpEnabled: helpAutomat.state?.enabled ?? false,
|
||||
pwaUpdateAvailable: pwaAutomat.state.needRefresh,
|
||||
@@ -37,6 +38,7 @@ class UserMenu extends PureComponent {
|
||||
this.handleToggleHelp = this.handleToggleHelp.bind(this);
|
||||
this.handleOpenPwaUpdate = this.handleOpenPwaUpdate.bind(this);
|
||||
this.handleLogout = this.handleLogout.bind(this);
|
||||
this.handleOpenSettingsEvent = this.handleOpenSettingsEvent.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
@@ -47,12 +49,23 @@ class UserMenu extends PureComponent {
|
||||
this.unsubscribePwa = pwaAutomat.subscribe(this, (state) => ({
|
||||
pwaUpdateAvailable: Boolean(state?.needRefresh),
|
||||
}));
|
||||
if (typeof window !== 'undefined') {
|
||||
window.addEventListener('open-settings', this.handleOpenSettingsEvent);
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.unsubscribeAuth?.();
|
||||
this.unsubscribeHelp?.();
|
||||
this.unsubscribePwa?.();
|
||||
if (typeof window !== 'undefined') {
|
||||
window.removeEventListener('open-settings', this.handleOpenSettingsEvent);
|
||||
}
|
||||
}
|
||||
|
||||
handleOpenSettingsEvent(e) {
|
||||
const tab = e?.detail?.tab;
|
||||
this.setState({ settingsOpen: true, settingsInitialTab: tab || null });
|
||||
}
|
||||
|
||||
handleOpenPwaUpdate() {
|
||||
@@ -74,11 +87,11 @@ class UserMenu extends PureComponent {
|
||||
}
|
||||
|
||||
handleOpenSettings() {
|
||||
this.setState({ anchorEl: null, settingsOpen: true });
|
||||
this.setState({ anchorEl: null, settingsOpen: true, settingsInitialTab: null });
|
||||
}
|
||||
|
||||
handleCloseSettings() {
|
||||
this.setState({ settingsOpen: false });
|
||||
this.setState({ settingsOpen: false, settingsInitialTab: null });
|
||||
}
|
||||
|
||||
handleToggleHelp(e) {
|
||||
@@ -98,7 +111,7 @@ class UserMenu extends PureComponent {
|
||||
}
|
||||
|
||||
render() {
|
||||
const { user, anchorEl, settingsOpen, helpEnabled, pwaUpdateAvailable } = this.state;
|
||||
const { user, anchorEl, settingsOpen, settingsInitialTab, helpEnabled, pwaUpdateAvailable } = this.state;
|
||||
const open = Boolean(anchorEl);
|
||||
|
||||
const pictureSrc = (() => {
|
||||
@@ -201,7 +214,7 @@ class UserMenu extends PureComponent {
|
||||
<ListItemText>Sign out</ListItemText>
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
<SettingsDialog open={settingsOpen} onClose={this.handleCloseSettings} />
|
||||
<SettingsDialog open={settingsOpen} initialTab={settingsInitialTab} onClose={this.handleCloseSettings} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -100,6 +100,9 @@ export default class SettingsDialog extends Component {
|
||||
if (open && !prevProps.open) {
|
||||
const currentVhost = Boolean(authAutomat.state.vhost);
|
||||
const updates = {};
|
||||
if (this.props.initialTab) {
|
||||
updates.tab = this.props.initialTab;
|
||||
}
|
||||
if (this.state.schemaVersion !== authAutomat.state.schemaVersion) {
|
||||
updates.schemaVersion = authAutomat.state.schemaVersion;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user