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,144 +69,161 @@ 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%',
bgcolor: CHAT_BUSY,
opacity: 0.5,
animation: 'fabAskPulse 1.5s cubic-bezier(0.4, 0, 0.6, 1) infinite',
'@keyframes fabAskPulse': {
'0%': { transform: 'scale(0.95)', opacity: 0.8 },
'50%': { transform: 'scale(1.35)', opacity: 0.12 },
'100%': { transform: 'scale(0.95)', opacity: 0.8 },
},
}}
/>
)}
{/* Animated pulsating halo when model is busy thinking/working */}
{!hasPendingAsk && isBusy && !isOpen && (
<Box
sx={{
position: 'absolute',
top: -6,
left: -6,
right: -6,
bottom: -6,
borderRadius: '50%',
bgcolor: 'warning.main',
opacity: 0.4,
animation: 'fabPulse 1.6s cubic-bezier(0.4, 0, 0.6, 1) infinite',
'@keyframes fabPulse': {
'0%': { transform: 'scale(0.95)', opacity: 0.7 },
'50%': { transform: 'scale(1.25)', opacity: 0.15 },
'100%': { transform: 'scale(0.95)', opacity: 0.7 },
},
}}
/>
)}
{/* Circular spinner ring when model is busy */}
{!hasPendingAsk && isBusy && !isOpen && (
<CircularProgress
size={66}
thickness={2.5}
sx={{
position: 'absolute',
top: -6,
left: -6,
color: CHAT_BUSY,
pointerEvents: 'none',
}}
/>
)}
<Badge
badgeContent={hasPendingAsk ? '?' : undefined}
color={hasPendingAsk ? 'error' : isBusy ? 'warning' : 'default'}
variant={hasPendingAsk ? 'standard' : 'dot'}
invisible={!hasPendingAsk && !isBusy}
overlap="circular"
anchorOrigin={{ vertical: 'top', horizontal: 'right' }}
sx={{
'& .MuiBadge-badge': hasPendingAsk
? {
bgcolor: RED,
color: WHITE,
fontWeight: 900,
fontSize: '0.85rem',
minWidth: 22,
height: 22,
borderRadius: '50%', borderRadius: '50%',
boxShadow: `0 2px 10px ${RED_GLOW}`, bgcolor: CHAT_BUSY,
animation: 'badgePop 1.5s ease infinite', opacity: 0.5,
'@keyframes badgePop': { animation: 'fabAskPulse 1.5s cubic-bezier(0.4, 0, 0.6, 1) infinite',
'0%, 100%': { transform: 'scale(1)' }, '@keyframes fabAskPulse': {
'50%': { transform: 'scale(1.2)' }, '0%': { transform: 'scale(0.95)', opacity: 0.8 },
'50%': { transform: 'scale(1.35)', opacity: 0.12 },
'100%': { transform: 'scale(0.95)', opacity: 0.8 },
}, },
} }}
: undefined, />
}} )}
>
<Fab {/* Animated pulsating halo when model is busy thinking/working */}
color="primary" {!hasPendingAsk && isBusy && !isOpen && (
aria-label={ariaLabel} <Box
sx={{
position: 'absolute',
top: -4,
left: -4,
right: -4,
bottom: -4,
borderRadius: '50%',
bgcolor: 'warning.main',
opacity: 0.4,
animation: 'fabPulse 1.6s cubic-bezier(0.4, 0, 0.6, 1) infinite',
'@keyframes fabPulse': {
'0%': { transform: 'scale(0.95)', opacity: 0.7 },
'50%': { transform: 'scale(1.25)', opacity: 0.15 },
'100%': { transform: 'scale(0.95)', opacity: 0.7 },
},
}}
/>
)}
{/* Circular spinner ring when model is busy */}
{!hasPendingAsk && isBusy && !isOpen && (
<CircularProgress
size={38}
thickness={3}
sx={{
position: 'absolute',
top: -3,
left: -3,
color: CHAT_BUSY,
pointerEvents: 'none',
}}
/>
)}
<IconButton
onClick={this.handleClick} onClick={this.handleClick}
sx={(theme) => ({ aria-label={ariaLabel}
width: 54, sx={{ p: 0, width: 32, height: 32, borderRadius: '50%' }}
height: 54,
boxShadow: hasPendingAsk
? CHAT_FAB_SHADOW_PENDING
: isBusy
? CHAT_FAB_SHADOW_BUSY
: CHAT_FAB_SHADOW_IDLE,
background: hasPendingAsk
? CHAT_FAB_PENDING
: isBusy
? CHAT_FAB_BUSY
: `linear-gradient(135deg, ${theme.palette.primary.main} 0%, ${theme.palette.primary.dark} 100%)`,
})}
> >
{isOpen ? ( <Badge
<CloseRoundedIcon sx={{ fontSize: 24 }} /> badgeContent={hasPendingAsk ? '?' : undefined}
) : hasPendingAsk ? ( color={hasPendingAsk ? 'error' : isBusy ? 'warning' : 'default'}
<HelpOutlineRoundedIcon variant={hasPendingAsk ? 'standard' : 'dot'}
sx={{ invisible={!hasPendingAsk && !isBusy}
fontSize: 26, overlap="circular"
animation: 'fabWiggle 2s ease infinite', anchorOrigin={{ vertical: 'top', horizontal: 'right' }}
'@keyframes fabWiggle': { sx={{
'0%, 100%': { transform: 'rotate(0deg)' }, '& .MuiBadge-badge': hasPendingAsk
'15%': { transform: 'rotate(-12deg)' }, ? {
'30%': { transform: 'rotate(12deg)' }, bgcolor: RED,
'45%': { transform: 'rotate(-8deg)' }, color: WHITE,
'60%': { transform: 'rotate(0deg)' }, fontWeight: 900,
}, fontSize: '0.75rem',
}} minWidth: 18,
/> height: 18,
) : isBusy ? ( borderRadius: '50%',
<AutoAwesomeRoundedIcon sx={{ fontSize: 24 }} /> boxShadow: `0 2px 8px ${RED_GLOW}`,
) : ( animation: 'badgePop 1.5s ease infinite',
<ChatRoundedIcon sx={{ fontSize: 24 }} /> '@keyframes badgePop': {
)} '0%, 100%': { transform: 'scale(1)' },
</Fab> '50%': { transform: 'scale(1.2)' },
</Badge> },
}
: undefined,
}}
>
<Avatar
sx={(theme) => ({
width: 32,
height: 32,
bgcolor: hasPendingAsk
? CHAT_FAB_PENDING
: isBusy
? CHAT_FAB_BUSY
: isOpen
? theme.palette.primary.main
: `linear-gradient(135deg, ${theme.palette.primary.main} 0%, ${theme.palette.primary.dark} 100%)`,
background: hasPendingAsk
? CHAT_FAB_PENDING
: isBusy
? CHAT_FAB_BUSY
: isOpen
? theme.palette.primary.main
: `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 ? (
<CloseRoundedIcon sx={{ fontSize: 18 }} />
) : hasPendingAsk ? (
<HelpOutlineRoundedIcon
sx={{
fontSize: 18,
animation: 'fabWiggle 2s ease infinite',
'@keyframes fabWiggle': {
'0%, 100%': { transform: 'rotate(0deg)' },
'15%': { transform: 'rotate(-12deg)' },
'30%': { transform: 'rotate(12deg)' },
'45%': { transform: 'rotate(-8deg)' },
'60%': { transform: 'rotate(0deg)' },
},
}}
/>
) : isBusy ? (
<AutoAwesomeRoundedIcon sx={{ fontSize: 18 }} />
) : (
<AutoAwesomeRoundedIcon sx={{ fontSize: 18 }} />
)}
</Avatar>
</Badge>
</IconButton>
</Box>
</Box> </Box>
</Box> );
);
} }
} }