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
* → { success: true, deletedJob: PrinterJob }

View File

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

View File

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

View File

@@ -10,6 +10,7 @@ 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 CancelScheduleSendOutlinedIcon from '@mui/icons-material/CancelScheduleSendOutlined';
import SettingsIcon from '@mui/icons-material/Settings';
import { enqueueSnackbar } from 'notistack';
import companyAutomat from '../automats/companyAutomat';
@@ -27,10 +28,12 @@ export default class OnlinePrinterIcons extends PureComponent {
: [],
menuAnchor: null,
testingId: null,
cancellingId: null,
};
this.handleOpenMenu = this.handleOpenMenu.bind(this);
this.handleCloseMenu = this.handleCloseMenu.bind(this);
this.handleTestPrint = this.handleTestPrint.bind(this);
this.handleCancelAllJobs = this.handleCancelAllJobs.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() {
this.handleCloseMenu();
window.dispatchEvent(
@@ -133,7 +159,7 @@ export default class OnlinePrinterIcons extends PureComponent {
}
render() {
const { menuAnchor, testingId } = this.state;
const { menuAnchor, testingId, cancellingId } = this.state;
const onlinePrinters = this.getOnlinePrinters();
if (onlinePrinters.length === 0) {
@@ -266,7 +292,7 @@ export default class OnlinePrinterIcons extends PureComponent {
<MenuItem
onClick={() => void this.handleTestPrint(activePrinter)}
disabled={testingId === activePrinter?.id}
disabled={testingId === activePrinter?.id || cancellingId === activePrinter?.id}
>
<ListItemIcon>
<PrintOutlinedIcon fontSize="small" color="primary" />
@@ -277,6 +303,19 @@ export default class OnlinePrinterIcons extends PureComponent {
/>
</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}>
<ListItemIcon>
<SettingsIcon fontSize="small" />

View File

@@ -1,10 +1,9 @@
import React, { PureComponent } from 'react';
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 Tooltip from '@mui/material/Tooltip';
import CircularProgress from '@mui/material/CircularProgress';
import ChatRoundedIcon from '@mui/icons-material/ChatRounded';
import CloseRoundedIcon from '@mui/icons-material/CloseRounded';
import AutoAwesomeRoundedIcon from '@mui/icons-material/AutoAwesomeRounded';
import HelpOutlineRoundedIcon from '@mui/icons-material/HelpOutlineRounded';
@@ -70,144 +69,161 @@ export default class ChatFab extends PureComponent {
return (
<Box
sx={(theme) => ({
position: 'fixed',
bottom: { xs: 20, sm: 28 },
left: { xs: 20, lg: 164 },
zIndex: theme.zIndex.speedDial || 1050,
})}
sx={{
px: { xs: 0.5, sm: 1, lg: 1.5 },
py: { xs: 0.5, lg: 1 },
alignSelf: { xs: 'center', lg: 'flex-start' },
display: 'flex',
alignItems: 'center',
}}
>
<Box sx={{ position: 'relative' }}>
{/* Animated pulsing halo when awaiting user answer */}
{hasPendingAsk && !isOpen && (
<Box
sx={{
position: 'absolute',
top: -8,
left: -8,
right: -8,
bottom: -8,
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,
<Box sx={{ position: 'relative', display: 'inline-flex' }}>
{/* Animated pulsing halo when awaiting user answer */}
{hasPendingAsk && !isOpen && (
<Box
sx={{
position: 'absolute',
top: -4,
left: -4,
right: -4,
bottom: -4,
borderRadius: '50%',
boxShadow: `0 2px 10px ${RED_GLOW}`,
animation: 'badgePop 1.5s ease infinite',
'@keyframes badgePop': {
'0%, 100%': { transform: 'scale(1)' },
'50%': { transform: 'scale(1.2)' },
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 },
},
}
: undefined,
}}
>
<Fab
color="primary"
aria-label={ariaLabel}
}}
/>
)}
{/* Animated pulsating halo when model is busy thinking/working */}
{!hasPendingAsk && isBusy && !isOpen && (
<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}
sx={(theme) => ({
width: 54,
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%)`,
})}
aria-label={ariaLabel}
sx={{ p: 0, width: 32, height: 32, borderRadius: '50%' }}
>
{isOpen ? (
<CloseRoundedIcon sx={{ fontSize: 24 }} />
) : hasPendingAsk ? (
<HelpOutlineRoundedIcon
sx={{
fontSize: 26,
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: 24 }} />
) : (
<ChatRoundedIcon sx={{ fontSize: 24 }} />
)}
</Fab>
</Badge>
<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.75rem',
minWidth: 18,
height: 18,
borderRadius: '50%',
boxShadow: `0 2px 8px ${RED_GLOW}`,
animation: 'badgePop 1.5s ease infinite',
'@keyframes badgePop': {
'0%, 100%': { transform: 'scale(1)' },
'50%': { transform: 'scale(1.2)' },
},
}
: 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>
);
);
}
}