u
This commit is contained in:
@@ -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 }
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user