This commit is contained in:
seb
2026-10-31 22:15:03 +01:00
parent ee80646c1c
commit 545eb98191
5 changed files with 229 additions and 139 deletions

View File

@@ -87,6 +87,37 @@ export const printerjob = {
}); });
}, },
/**
* POST /api/printerjob/cancel-all
* Cancels all pending/processing jobs, optionally scoped to an agentprinter.
* Falls back to listing and cancelling individual jobs if needed.
* @param {{ agentprinterId?: number }} [payload]
* → { success: true, cancelledCount: number, jobs: PrinterJob[] }
*/
async cancelAll({ agentprinterId } = {}) {
try {
return await http('/api/printerjob/cancel-all', {
method: 'POST',
body: agentprinterId != null ? { agentprinterId: Number(agentprinterId) } : {},
});
} catch (err) {
if (err?.status === 404) {
const { printerjobs = [] } = await this.list({ agentprinterId });
const cancellable = printerjobs.filter(
(j) => j.state === 'pending' || j.state === 'processing',
);
if (cancellable.length === 0) return { success: true, cancelledCount: 0, jobs: [] };
const results = await Promise.all(cancellable.map((j) => this.cancel(j.id)));
return {
success: true,
cancelledCount: results.length,
jobs: results.map((r) => r.printerjob),
};
}
throw err;
}
},
/** /**
* DELETE /api/printerjob/:id * DELETE /api/printerjob/:id
* → { success: true, deletedJob: PrinterJob } * → { success: true, deletedJob: PrinterJob }

View File

@@ -881,6 +881,10 @@ const actions = {
return printerjobApi.sendTestZpl(agentprinterId, options); return printerjobApi.sendTestZpl(agentprinterId, options);
}, },
async cancelAllPrinterJobs(agentprinterId) {
return printerjobApi.cancelAll({ agentprinterId });
},
async createZplJob(payload) { async createZplJob(payload) {
return printerjobApi.createZplJob(payload); return printerjobApi.createZplJob(payload);
}, },

View File

@@ -212,6 +212,7 @@ class AppSidebar extends PureComponent {
> >
<OnlinePrinterIcons /> <OnlinePrinterIcons />
<UserMenu /> <UserMenu />
<ChatFab />
</Box> </Box>
</Box> </Box>
); );
@@ -430,7 +431,6 @@ export default function AppShell() {
</Suspense> </Suspense>
</Box> </Box>
</Box> </Box>
<ChatFab />
<ChatDrawer /> <ChatDrawer />
<PinnedAppletFabs pathname={pathname} /> <PinnedAppletFabs pathname={pathname} />
<PinnedAppletDialog /> <PinnedAppletDialog />

View File

@@ -10,6 +10,7 @@ import Menu from '@mui/material/Menu';
import MenuItem from '@mui/material/MenuItem'; import MenuItem from '@mui/material/MenuItem';
import Typography from '@mui/material/Typography'; import Typography from '@mui/material/Typography';
import PrintOutlinedIcon from '@mui/icons-material/PrintOutlined'; import PrintOutlinedIcon from '@mui/icons-material/PrintOutlined';
import CancelScheduleSendOutlinedIcon from '@mui/icons-material/CancelScheduleSendOutlined';
import SettingsIcon from '@mui/icons-material/Settings'; import SettingsIcon from '@mui/icons-material/Settings';
import { enqueueSnackbar } from 'notistack'; import { enqueueSnackbar } from 'notistack';
import companyAutomat from '../automats/companyAutomat'; import companyAutomat from '../automats/companyAutomat';
@@ -27,10 +28,12 @@ export default class OnlinePrinterIcons extends PureComponent {
: [], : [],
menuAnchor: null, menuAnchor: null,
testingId: null, testingId: null,
cancellingId: null,
}; };
this.handleOpenMenu = this.handleOpenMenu.bind(this); this.handleOpenMenu = this.handleOpenMenu.bind(this);
this.handleCloseMenu = this.handleCloseMenu.bind(this); this.handleCloseMenu = this.handleCloseMenu.bind(this);
this.handleTestPrint = this.handleTestPrint.bind(this); this.handleTestPrint = this.handleTestPrint.bind(this);
this.handleCancelAllJobs = this.handleCancelAllJobs.bind(this);
this.handleOpenPrinterSettings = this.handleOpenPrinterSettings.bind(this); this.handleOpenPrinterSettings = this.handleOpenPrinterSettings.bind(this);
} }
@@ -125,6 +128,29 @@ export default class OnlinePrinterIcons extends PureComponent {
} }
} }
async handleCancelAllJobs(printer) {
if (!printer?.id) return;
this.setState({ cancellingId: printer.id });
try {
const res = await companyAutomat.actions.cancelAllPrinterJobs(printer.id);
const count = res?.cancelledCount ?? 0;
if (count > 0) {
enqueueSnackbar(
`${count} Druckauftrag/Druckaufträge für ${printer.model || 'Drucker'} abgebrochen.`,
{ variant: 'info' },
);
} else {
enqueueSnackbar(`Keine offenen Aufträge für ${printer.model || 'Drucker'}.`, {
variant: 'info',
});
}
} catch (err) {
notifyError(err);
} finally {
this.setState({ cancellingId: null, menuAnchor: null });
}
}
handleOpenPrinterSettings() { handleOpenPrinterSettings() {
this.handleCloseMenu(); this.handleCloseMenu();
window.dispatchEvent( window.dispatchEvent(
@@ -133,7 +159,7 @@ export default class OnlinePrinterIcons extends PureComponent {
} }
render() { render() {
const { menuAnchor, testingId } = this.state; const { menuAnchor, testingId, cancellingId } = this.state;
const onlinePrinters = this.getOnlinePrinters(); const onlinePrinters = this.getOnlinePrinters();
if (onlinePrinters.length === 0) { if (onlinePrinters.length === 0) {
@@ -266,7 +292,7 @@ export default class OnlinePrinterIcons extends PureComponent {
<MenuItem <MenuItem
onClick={() => void this.handleTestPrint(activePrinter)} onClick={() => void this.handleTestPrint(activePrinter)}
disabled={testingId === activePrinter?.id} disabled={testingId === activePrinter?.id || cancellingId === activePrinter?.id}
> >
<ListItemIcon> <ListItemIcon>
<PrintOutlinedIcon fontSize="small" color="primary" /> <PrintOutlinedIcon fontSize="small" color="primary" />
@@ -277,6 +303,19 @@ export default class OnlinePrinterIcons extends PureComponent {
/> />
</MenuItem> </MenuItem>
<MenuItem
onClick={() => void this.handleCancelAllJobs(activePrinter)}
disabled={cancellingId === activePrinter?.id || testingId === activePrinter?.id}
>
<ListItemIcon>
<CancelScheduleSendOutlinedIcon fontSize="small" color="error" />
</ListItemIcon>
<ListItemText
primary={cancellingId === activePrinter?.id ? 'Wird abgebrochen…' : 'Alle Aufträge abbrechen'}
secondary="Warteschlange leeren"
/>
</MenuItem>
<MenuItem onClick={this.handleOpenPrinterSettings}> <MenuItem onClick={this.handleOpenPrinterSettings}>
<ListItemIcon> <ListItemIcon>
<SettingsIcon fontSize="small" /> <SettingsIcon fontSize="small" />

View File

@@ -1,10 +1,9 @@
import React, { PureComponent } from 'react'; import React, { PureComponent } from 'react';
import Box from '@mui/material/Box'; import Box from '@mui/material/Box';
import Fab from '@mui/material/Fab'; import Avatar from '@mui/material/Avatar';
import IconButton from '@mui/material/IconButton';
import Badge from '@mui/material/Badge'; import Badge from '@mui/material/Badge';
import Tooltip from '@mui/material/Tooltip';
import CircularProgress from '@mui/material/CircularProgress'; import CircularProgress from '@mui/material/CircularProgress';
import ChatRoundedIcon from '@mui/icons-material/ChatRounded';
import CloseRoundedIcon from '@mui/icons-material/CloseRounded'; import CloseRoundedIcon from '@mui/icons-material/CloseRounded';
import AutoAwesomeRoundedIcon from '@mui/icons-material/AutoAwesomeRounded'; import AutoAwesomeRoundedIcon from '@mui/icons-material/AutoAwesomeRounded';
import HelpOutlineRoundedIcon from '@mui/icons-material/HelpOutlineRounded'; import HelpOutlineRoundedIcon from '@mui/icons-material/HelpOutlineRounded';
@@ -70,23 +69,24 @@ export default class ChatFab extends PureComponent {
return ( return (
<Box <Box
sx={(theme) => ({ sx={{
position: 'fixed', px: { xs: 0.5, sm: 1, lg: 1.5 },
bottom: { xs: 20, sm: 28 }, py: { xs: 0.5, lg: 1 },
left: { xs: 20, lg: 164 }, alignSelf: { xs: 'center', lg: 'flex-start' },
zIndex: theme.zIndex.speedDial || 1050, display: 'flex',
})} alignItems: 'center',
}}
> >
<Box sx={{ position: 'relative' }}> <Box sx={{ position: 'relative', display: 'inline-flex' }}>
{/* Animated pulsing halo when awaiting user answer */} {/* Animated pulsing halo when awaiting user answer */}
{hasPendingAsk && !isOpen && ( {hasPendingAsk && !isOpen && (
<Box <Box
sx={{ sx={{
position: 'absolute', position: 'absolute',
top: -8, top: -4,
left: -8, left: -4,
right: -8, right: -4,
bottom: -8, bottom: -4,
borderRadius: '50%', borderRadius: '50%',
bgcolor: CHAT_BUSY, bgcolor: CHAT_BUSY,
opacity: 0.5, opacity: 0.5,
@@ -105,10 +105,10 @@ export default class ChatFab extends PureComponent {
<Box <Box
sx={{ sx={{
position: 'absolute', position: 'absolute',
top: -6, top: -4,
left: -6, left: -4,
right: -6, right: -4,
bottom: -6, bottom: -4,
borderRadius: '50%', borderRadius: '50%',
bgcolor: 'warning.main', bgcolor: 'warning.main',
opacity: 0.4, opacity: 0.4,
@@ -125,18 +125,23 @@ export default class ChatFab extends PureComponent {
{/* Circular spinner ring when model is busy */} {/* Circular spinner ring when model is busy */}
{!hasPendingAsk && isBusy && !isOpen && ( {!hasPendingAsk && isBusy && !isOpen && (
<CircularProgress <CircularProgress
size={66} size={38}
thickness={2.5} thickness={3}
sx={{ sx={{
position: 'absolute', position: 'absolute',
top: -6, top: -3,
left: -6, left: -3,
color: CHAT_BUSY, color: CHAT_BUSY,
pointerEvents: 'none', pointerEvents: 'none',
}} }}
/> />
)} )}
<IconButton
onClick={this.handleClick}
aria-label={ariaLabel}
sx={{ p: 0, width: 32, height: 32, borderRadius: '50%' }}
>
<Badge <Badge
badgeContent={hasPendingAsk ? '?' : undefined} badgeContent={hasPendingAsk ? '?' : undefined}
color={hasPendingAsk ? 'error' : isBusy ? 'warning' : 'default'} color={hasPendingAsk ? 'error' : isBusy ? 'warning' : 'default'}
@@ -150,11 +155,11 @@ export default class ChatFab extends PureComponent {
bgcolor: RED, bgcolor: RED,
color: WHITE, color: WHITE,
fontWeight: 900, fontWeight: 900,
fontSize: '0.85rem', fontSize: '0.75rem',
minWidth: 22, minWidth: 18,
height: 22, height: 18,
borderRadius: '50%', borderRadius: '50%',
boxShadow: `0 2px 10px ${RED_GLOW}`, boxShadow: `0 2px 8px ${RED_GLOW}`,
animation: 'badgePop 1.5s ease infinite', animation: 'badgePop 1.5s ease infinite',
'@keyframes badgePop': { '@keyframes badgePop': {
'0%, 100%': { transform: 'scale(1)' }, '0%, 100%': { transform: 'scale(1)' },
@@ -164,31 +169,41 @@ export default class ChatFab extends PureComponent {
: undefined, : undefined,
}} }}
> >
<Fab <Avatar
color="primary"
aria-label={ariaLabel}
onClick={this.handleClick}
sx={(theme) => ({ sx={(theme) => ({
width: 54, width: 32,
height: 54, height: 32,
boxShadow: hasPendingAsk bgcolor: hasPendingAsk
? CHAT_FAB_SHADOW_PENDING ? CHAT_FAB_PENDING
: isBusy : isBusy
? CHAT_FAB_SHADOW_BUSY ? CHAT_FAB_BUSY
: CHAT_FAB_SHADOW_IDLE, : isOpen
? theme.palette.primary.main
: `linear-gradient(135deg, ${theme.palette.primary.main} 0%, ${theme.palette.primary.dark} 100%)`,
background: hasPendingAsk background: hasPendingAsk
? CHAT_FAB_PENDING ? CHAT_FAB_PENDING
: isBusy : isBusy
? CHAT_FAB_BUSY ? CHAT_FAB_BUSY
: isOpen
? theme.palette.primary.main
: `linear-gradient(135deg, ${theme.palette.primary.main} 0%, ${theme.palette.primary.dark} 100%)`, : `linear-gradient(135deg, ${theme.palette.primary.main} 0%, ${theme.palette.primary.dark} 100%)`,
color: WHITE,
transition: 'all 0.2s ease',
boxShadow: hasPendingAsk
? CHAT_FAB_SHADOW_PENDING
: isBusy
? CHAT_FAB_SHADOW_BUSY
: isOpen
? `0 2px 8px rgba(0,0,0,0.25)`
: 'none',
})} })}
> >
{isOpen ? ( {isOpen ? (
<CloseRoundedIcon sx={{ fontSize: 24 }} /> <CloseRoundedIcon sx={{ fontSize: 18 }} />
) : hasPendingAsk ? ( ) : hasPendingAsk ? (
<HelpOutlineRoundedIcon <HelpOutlineRoundedIcon
sx={{ sx={{
fontSize: 26, fontSize: 18,
animation: 'fabWiggle 2s ease infinite', animation: 'fabWiggle 2s ease infinite',
'@keyframes fabWiggle': { '@keyframes fabWiggle': {
'0%, 100%': { transform: 'rotate(0deg)' }, '0%, 100%': { transform: 'rotate(0deg)' },
@@ -200,12 +215,13 @@ export default class ChatFab extends PureComponent {
}} }}
/> />
) : isBusy ? ( ) : isBusy ? (
<AutoAwesomeRoundedIcon sx={{ fontSize: 24 }} /> <AutoAwesomeRoundedIcon sx={{ fontSize: 18 }} />
) : ( ) : (
<ChatRoundedIcon sx={{ fontSize: 24 }} /> <AutoAwesomeRoundedIcon sx={{ fontSize: 18 }} />
)} )}
</Fab> </Avatar>
</Badge> </Badge>
</IconButton>
</Box> </Box>
</Box> </Box>
); );